use riverpod

This commit is contained in:
Simon Ding
2024-07-09 13:51:30 +08:00
parent a76adfdd29
commit 684846fd88
8 changed files with 327 additions and 293 deletions

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:ui/navdrawer.dart'; import 'package:ui/navdrawer.dart';
import 'package:ui/search.dart'; import 'package:ui/search.dart';
@@ -46,7 +47,7 @@ class MyApp extends StatelessWidget {
// Center is a layout widget. It takes a single child and positions it // Center is a layout widget. It takes a single child and positions it
// in the middle of the parent. // in the middle of the parent.
child: Row(children: <Widget>[ child: Row(children: <Widget>[
NavDrawer(), const NavDrawer(),
const VerticalDivider(thickness: 1, width: 1), const VerticalDivider(thickness: 1, width: 1),
Expanded(child: child) Expanded(child: child)
]))), ]))),
@@ -81,7 +82,8 @@ class MyApp extends StatelessWidget {
], ],
); );
return MaterialApp.router( return ProviderScope(
child: MaterialApp.router(
title: 'Flutter Demo', title: 'Flutter Demo',
theme: ThemeData( theme: ThemeData(
// This is the theme of your application. // This is the theme of your application.
@@ -104,6 +106,7 @@ class MyApp extends StatelessWidget {
useMaterial3: true, useMaterial3: true,
), ),
routerConfig: _router, routerConfig: _router,
),
); );
} }
} }

View File

@@ -0,0 +1,82 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/APIs.dart';
import 'package:ui/server_response.dart';
var seriesDetailsProvider = FutureProvider.family((ref, seriesId) async {
var resp = await Dio().get("${APIs.seriesDetailUrl}$seriesId");
var rsp = ServerResponse.fromJson(resp.data);
if (rsp.code != 0) {
throw rsp.message;
}
return SeriesDetails.fromJson(rsp.data);
});
class SeriesDetails {
int? id;
int? tmdbId;
String? name;
String? originalName;
String? overview;
String? path;
String? posterPath;
String? createdAt;
List<Episodes>? episodes;
SeriesDetails(
{this.id,
this.tmdbId,
this.name,
this.originalName,
this.overview,
this.path,
this.posterPath,
this.createdAt,
this.episodes});
SeriesDetails.fromJson(Map<String, dynamic> json) {
id = json['id'];
tmdbId = json['tmdb_id'];
name = json['name'];
originalName = json['original_name'];
overview = json['overview'];
path = json['path'];
posterPath = json['poster_path'];
createdAt = json['created_at'];
if (json['episodes'] != null) {
episodes = <Episodes>[];
json['episodes'].forEach((v) {
episodes!.add(Episodes.fromJson(v));
});
}
}
}
class Episodes {
int? id;
int? seriesId;
int? episodeNumber;
String? title;
String? airDate;
int? seasonNumber;
String? overview;
Episodes(
{this.id,
this.seriesId,
this.episodeNumber,
this.title,
this.airDate,
this.seasonNumber,
this.overview});
Episodes.fromJson(Map<String, dynamic> json) {
id = json['id'];
seriesId = json['series_id'];
episodeNumber = json['episode_number'];
title = json['title'];
airDate = json['air_date'];
seasonNumber = json['season_number'];
overview = json['overview'];
}
}

View File

@@ -0,0 +1,58 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/APIs.dart';
import 'package:ui/server_response.dart';
final welcomePageDataProvider = FutureProvider((ref) async {
var resp = await Dio().get(APIs.watchlistUrl);
var sp = ServerResponse.fromJson(resp.data);
List<TvSeries> favList = List.empty(growable: true);
for (var item in sp.data as List) {
var tv = TvSeries.fromJson(item);
favList.add(tv);
}
return favList;
});
class TvSeries {
int? id;
int? tmdbId;
String? name;
String? originalName;
String? overview;
String? path;
String? posterPath;
TvSeries(
{this.id,
this.tmdbId,
this.name,
this.originalName,
this.overview,
this.path,
this.posterPath});
TvSeries.fromJson(Map<String, dynamic> json) {
id = json['id'];
tmdbId = json['tmdb_id'];
name = json['name'];
originalName = json['original_name'];
overview = json['overview'];
path = json['path'];
posterPath = json["poster_path"];
}
}
var tmdbApiSettingProvider = FutureProvider(
(ref) async {
final dio = Dio();
var resp = await dio
.get(APIs.settingsUrl, queryParameters: {"key": APIs.tmdbApiKey});
var rrr = resp.data as Map<String, dynamic>;
var data = rrr["data"] as Map<String, dynamic>;
var key = data[APIs.tmdbApiKey] as String;
return key;
},
);

View File

@@ -1,35 +1,38 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/APIs.dart'; import 'package:ui/APIs.dart';
import 'package:ui/providers/welcome_data.dart';
import 'package:ui/server_response.dart'; import 'package:ui/server_response.dart';
import 'package:ui/utils.dart'; import 'package:ui/utils.dart';
class SystemSettingsPage extends StatefulWidget { class SystemSettingsPage extends ConsumerStatefulWidget {
static const route = "/systemsettings"; static const route = "/systemsettings";
const SystemSettingsPage({super.key}); const SystemSettingsPage({super.key});
@override @override
State<StatefulWidget> createState() { ConsumerState<ConsumerStatefulWidget> createState() {
return _SystemSettingsPageState(); return _SystemSettingsPageState();
} }
} }
class _SystemSettingsPageState extends State<SystemSettingsPage> { class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
final GlobalKey _formKey = GlobalKey<FormState>(); final GlobalKey _formKey = GlobalKey<FormState>();
final TextEditingController _tmdbApiKeyController = TextEditingController();
List<dynamic> indexers = List.empty();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_handleRefresh();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( var key = ref.watch(tmdbApiSettingProvider);
return key.when(
data: (data ) => Container(
padding: const EdgeInsets.fromLTRB(40, 10, 40, 0), padding: const EdgeInsets.fromLTRB(40, 10, 40, 0),
child: RefreshIndicator(
onRefresh: _handleRefresh,
child: Form( child: Form(
key: _formKey, //设置globalKey用于后面获取FormState key: _formKey, //设置globalKey用于后面获取FormState
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
@@ -37,7 +40,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
children: [ children: [
TextFormField( TextFormField(
autofocus: true, autofocus: true,
controller: _tmdbApiKeyController, initialValue: data,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: "TMDB Api Key", labelText: "TMDB Api Key",
icon: Icon(Icons.key), icon: Icon(Icons.key),
@@ -46,6 +49,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
validator: (v) { validator: (v) {
return v!.trim().isNotEmpty ? null : "ApiKey 不能为空"; return v!.trim().isNotEmpty ? null : "ApiKey 不能为空";
}, },
onSaved: (newValue) {
_submitSettings(context, newValue!);
},
), ),
Center( Center(
child: Padding( child: Padding(
@@ -60,7 +66,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
// 调用validate()方法校验用户名密码是否合法,校验 // 调用validate()方法校验用户名密码是否合法,校验
// 通过后再提交数据。 // 通过后再提交数据。
if ((_formKey.currentState as FormState).validate()) { if ((_formKey.currentState as FormState).validate()) {
_submitSettings(context, _tmdbApiKeyController.text); (_formKey.currentState as FormState).save();
} }
}, },
), ),
@@ -68,20 +74,10 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
) )
], ],
), ),
)), ),
); ),
} error: (err, trace) => Text("$err"),
loading: () => const CircularProgressIndicator());
Future<void> _handleRefresh() async {
final dio = Dio();
var resp = await dio
.get(APIs.settingsUrl, queryParameters: {"key": APIs.tmdbApiKey});
var rrr = resp.data as Map<String, dynamic>;
var data = rrr["data"] as Map<String, dynamic>;
var key = data[APIs.tmdbApiKey] as String;
_tmdbApiKeyController.text = key;
// Fetch new data and update the UI
} }
void _submitSettings(BuildContext context, String v) async { void _submitSettings(BuildContext context, String v) async {

View File

@@ -1,10 +1,12 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/APIs.dart'; import 'package:ui/APIs.dart';
import 'package:ui/providers/series_details.dart';
import 'package:ui/server_response.dart'; import 'package:ui/server_response.dart';
import 'package:ui/utils.dart'; import 'package:ui/utils.dart';
class TvDetailsPage extends StatefulWidget { class TvDetailsPage extends ConsumerStatefulWidget {
static const route = "/series/:id"; static const route = "/series/:id";
static String toRoute(int id) { static String toRoute(int id) {
@@ -16,34 +18,28 @@ class TvDetailsPage extends StatefulWidget {
const TvDetailsPage({super.key, required this.seriesId}); const TvDetailsPage({super.key, required this.seriesId});
@override @override
State<StatefulWidget> createState() { ConsumerState<ConsumerStatefulWidget> createState() {
return _TvDetailsPageState(seriesId: seriesId); return _TvDetailsPageState(seriesId: seriesId);
} }
} }
class _TvDetailsPageState extends State<TvDetailsPage> { class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
final String seriesId; final String seriesId;
_TvDetailsPageState({required this.seriesId}); _TvDetailsPageState({required this.seriesId});
SeriesDetails? details;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_querySeriesDetails();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (details == null) { var seriesDetails = ref.watch(seriesDetailsProvider(seriesId));
return const Center( return seriesDetails.when(
child: Text("nothing here"), data: (details) {
);
}
Map<int, List<Widget>> m = Map(); Map<int, List<Widget>> m = Map();
for (final ep in details!.episodes!) { for (final ep in details.episodes!) {
var w = Container( var w = Container(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Row( child: Row(
@@ -63,8 +59,8 @@ class _TvDetailsPageState extends State<TvDetailsPage> {
const Expanded(child: Text("")), const Expanded(child: Text("")),
IconButton( IconButton(
onPressed: () { onPressed: () {
_searchAndDownload( _searchAndDownload(context, seriesId, ep.seasonNumber!,
context, seriesId, ep.seasonNumber!, ep.episodeNumber!); ep.episodeNumber!);
}, },
icon: const Icon(Icons.search)) icon: const Icon(Icons.search))
], ],
@@ -97,7 +93,6 @@ class _TvDetailsPageState extends State<TvDetailsPage> {
); );
list.add(seasonList); list.add(seasonList);
} }
return ListView( return ListView(
children: [ children: [
Card( Card(
@@ -135,21 +130,13 @@ class _TvDetailsPageState extends State<TvDetailsPage> {
Column( Column(
children: list, children: list,
), ),
], ],
); );
} },
error: (err, trace) {
void _querySeriesDetails() async { return Text("$err");
if (details != null) { },
return; loading: () => const CircularProgressIndicator());
}
var resp = await Dio().get("${APIs.seriesDetailUrl}$seriesId");
var rsp = ServerResponse.fromJson(resp.data);
setState(() {
details = SeriesDetails.fromJson(rsp.data);
});
} }
void _searchAndDownload(BuildContext context, String seriesId, int seasonNum, void _searchAndDownload(BuildContext context, String seriesId, int seasonNum,
@@ -170,72 +157,3 @@ class _TvDetailsPageState extends State<TvDetailsPage> {
} }
} }
} }
class SeriesDetails {
int? id;
int? tmdbId;
String? name;
String? originalName;
String? overview;
String? path;
String? posterPath;
String? createdAt;
List<Episodes>? episodes;
SeriesDetails(
{this.id,
this.tmdbId,
this.name,
this.originalName,
this.overview,
this.path,
this.posterPath,
this.createdAt,
this.episodes});
SeriesDetails.fromJson(Map<String, dynamic> json) {
id = json['id'];
tmdbId = json['tmdb_id'];
name = json['name'];
originalName = json['original_name'];
overview = json['overview'];
path = json['path'];
posterPath = json['poster_path'];
createdAt = json['created_at'];
if (json['episodes'] != null) {
episodes = <Episodes>[];
json['episodes'].forEach((v) {
episodes!.add(Episodes.fromJson(v));
});
}
}
}
class Episodes {
int? id;
int? seriesId;
int? episodeNumber;
String? title;
String? airDate;
int? seasonNumber;
String? overview;
Episodes(
{this.id,
this.seriesId,
this.episodeNumber,
this.title,
this.airDate,
this.seasonNumber,
this.overview});
Episodes.fromJson(Map<String, dynamic> json) {
id = json['id'];
seriesId = json['series_id'];
episodeNumber = json['episode_number'];
title = json['title'];
airDate = json['air_date'];
seasonNumber = json['season_number'];
overview = json['overview'];
}
}

View File

@@ -1,37 +1,26 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:ui/APIs.dart'; import 'package:ui/APIs.dart';
import 'package:ui/server_response.dart'; import 'package:ui/providers/welcome_data.dart';
import 'package:ui/tv_details.dart'; import 'package:ui/tv_details.dart';
class WelcomePage extends StatefulWidget { class WelcomePage extends ConsumerWidget {
const WelcomePage({super.key});
static const route = "/welcome"; static const route = "/welcome";
@override const WelcomePage({super.key});
State<StatefulWidget> createState() {
return _WeclomePageState();
}
}
class _WeclomePageState extends State<WelcomePage> {
var favList = List.empty(growable: true);
@override @override
void initState() { Widget build(BuildContext context, WidgetRef ref) {
super.initState(); final data = ref.watch(welcomePageDataProvider);
_onRefresh();
}
@override return switch (data) {
Widget build(BuildContext context) { AsyncData(:final value) => GridView.builder(
return GridView.builder( itemCount: value.length,
itemCount: favList.length,
gridDelegate: gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 6), const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 6),
itemBuilder: (context, i) { itemBuilder: (context, i) {
var item = TvSeries.fromJson(favList[i]); var item = value[i];
return Card( return Card(
margin: const EdgeInsets.all(4), margin: const EdgeInsets.all(4),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
@@ -63,46 +52,9 @@ class _WeclomePageState extends State<WelcomePage> {
], ],
), ),
)); ));
}); }),
} _ => const CircularProgressIndicator(),
Future<void> _onRefresh() async { };
if (favList.isNotEmpty) {
return;
}
var resp = await Dio().get(APIs.watchlistUrl);
var sp = ServerResponse.fromJson(resp.data);
setState(() {
favList = sp.data as List;
});
}
}
class TvSeries {
int? id;
int? tmdbId;
String? name;
String? originalName;
String? overview;
String? path;
String? posterPath;
TvSeries(
{this.id,
this.tmdbId,
this.name,
this.originalName,
this.overview,
this.path,
this.posterPath});
TvSeries.fromJson(Map<String, dynamic> json) {
id = json['id'];
tmdbId = json['tmdb_id'];
name = json['name'];
originalName = json['original_name'];
overview = json['overview'];
path = json['path'];
posterPath = json["poster_path"];
} }
} }

View File

@@ -78,6 +78,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.1"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -176,6 +184,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.9.0" version: "1.9.0"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.1"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -197,6 +213,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.11.1" version: "1.11.1"
state_notifier:
dependency: transitive
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:

View File

@@ -37,6 +37,7 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6 cupertino_icons: ^1.0.6
go_router: ^14.2.0 go_router: ^14.2.0
flutter_riverpod: ^2.5.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: