diff --git a/ui/lib/main.dart b/ui/lib/main.dart index ff06608..173e5ab 100644 --- a/ui/lib/main.dart +++ b/ui/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:ui/navdrawer.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 // in the middle of the parent. child: Row(children: [ - NavDrawer(), + const NavDrawer(), const VerticalDivider(thickness: 1, width: 1), Expanded(child: child) ]))), @@ -81,29 +82,31 @@ class MyApp extends StatelessWidget { ], ); - return MaterialApp.router( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.blue, brightness: Brightness.dark), - useMaterial3: true, + return ProviderScope( + child: MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + // This is the theme of your application. + // + // TRY THIS: Try running your application with "flutter run". You'll see + // the application has a purple toolbar. Then, without quitting the app, + // try changing the seedColor in the colorScheme below to Colors.green + // and then invoke "hot reload" (save your changes or press the "hot + // reload" button in a Flutter-supported IDE, or press "r" if you used + // the command line to start the app). + // + // Notice that the counter didn't reset back to zero; the application + // state is not lost during the reload. To reset the state, use hot + // restart instead. + // + // This works for code too, not just values: Most code changes can be + // tested with just a hot reload. + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.blue, brightness: Brightness.dark), + useMaterial3: true, + ), + routerConfig: _router, ), - routerConfig: _router, ); } } diff --git a/ui/lib/providers/series_details.dart b/ui/lib/providers/series_details.dart new file mode 100644 index 0000000..81b7df9 --- /dev/null +++ b/ui/lib/providers/series_details.dart @@ -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; + + SeriesDetails( + {this.id, + this.tmdbId, + this.name, + this.originalName, + this.overview, + this.path, + this.posterPath, + this.createdAt, + this.episodes}); + + SeriesDetails.fromJson(Map 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 = []; + 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 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']; + } +} diff --git a/ui/lib/providers/welcome_data.dart b/ui/lib/providers/welcome_data.dart new file mode 100644 index 0000000..d89d53e --- /dev/null +++ b/ui/lib/providers/welcome_data.dart @@ -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 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 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; + var data = rrr["data"] as Map; + var key = data[APIs.tmdbApiKey] as String; + + return key; + }, +); + diff --git a/ui/lib/system_settings.dart b/ui/lib/system_settings.dart index 0b99bc0..802d87d 100644 --- a/ui/lib/system_settings.dart +++ b/ui/lib/system_settings.dart @@ -1,35 +1,38 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ui/APIs.dart'; +import 'package:ui/providers/welcome_data.dart'; import 'package:ui/server_response.dart'; import 'package:ui/utils.dart'; -class SystemSettingsPage extends StatefulWidget { +class SystemSettingsPage extends ConsumerStatefulWidget { static const route = "/systemsettings"; const SystemSettingsPage({super.key}); @override - State createState() { + ConsumerState createState() { return _SystemSettingsPageState(); } } -class _SystemSettingsPageState extends State { +class _SystemSettingsPageState extends ConsumerState { final GlobalKey _formKey = GlobalKey(); - final TextEditingController _tmdbApiKeyController = TextEditingController(); + + List indexers = List.empty(); @override void initState() { super.initState(); - _handleRefresh(); } @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.fromLTRB(40, 10, 40, 0), - child: RefreshIndicator( - onRefresh: _handleRefresh, + var key = ref.watch(tmdbApiSettingProvider); + + return key.when( + data: (data ) => Container( + padding: const EdgeInsets.fromLTRB(40, 10, 40, 0), child: Form( key: _formKey, //设置globalKey,用于后面获取FormState autovalidateMode: AutovalidateMode.onUserInteraction, @@ -37,7 +40,7 @@ class _SystemSettingsPageState extends State { children: [ TextFormField( autofocus: true, - controller: _tmdbApiKeyController, + initialValue: data, decoration: const InputDecoration( labelText: "TMDB Api Key", icon: Icon(Icons.key), @@ -46,6 +49,9 @@ class _SystemSettingsPageState extends State { validator: (v) { return v!.trim().isNotEmpty ? null : "ApiKey 不能为空"; }, + onSaved: (newValue) { + _submitSettings(context, newValue!); + }, ), Center( child: Padding( @@ -60,7 +66,7 @@ class _SystemSettingsPageState extends State { // 调用validate()方法校验用户名密码是否合法,校验 // 通过后再提交数据。 if ((_formKey.currentState as FormState).validate()) { - _submitSettings(context, _tmdbApiKeyController.text); + (_formKey.currentState as FormState).save(); } }, ), @@ -68,20 +74,10 @@ class _SystemSettingsPageState extends State { ) ], ), - )), - ); - } - - Future _handleRefresh() async { - final dio = Dio(); - var resp = await dio - .get(APIs.settingsUrl, queryParameters: {"key": APIs.tmdbApiKey}); - var rrr = resp.data as Map; - var data = rrr["data"] as Map; - var key = data[APIs.tmdbApiKey] as String; - _tmdbApiKeyController.text = key; - - // Fetch new data and update the UI + ), + ), + error: (err, trace) => Text("$err"), + loading: () => const CircularProgressIndicator()); } void _submitSettings(BuildContext context, String v) async { diff --git a/ui/lib/tv_details.dart b/ui/lib/tv_details.dart index 9ea74a9..18a1a66 100644 --- a/ui/lib/tv_details.dart +++ b/ui/lib/tv_details.dart @@ -1,10 +1,12 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ui/APIs.dart'; +import 'package:ui/providers/series_details.dart'; import 'package:ui/server_response.dart'; import 'package:ui/utils.dart'; -class TvDetailsPage extends StatefulWidget { +class TvDetailsPage extends ConsumerStatefulWidget { static const route = "/series/:id"; static String toRoute(int id) { @@ -16,140 +18,125 @@ class TvDetailsPage extends StatefulWidget { const TvDetailsPage({super.key, required this.seriesId}); @override - State createState() { + ConsumerState createState() { return _TvDetailsPageState(seriesId: seriesId); } } -class _TvDetailsPageState extends State { +class _TvDetailsPageState extends ConsumerState { final String seriesId; _TvDetailsPageState({required this.seriesId}); - SeriesDetails? details; - @override void initState() { super.initState(); - _querySeriesDetails(); } @override Widget build(BuildContext context) { - if (details == null) { - return const Center( - child: Text("nothing here"), - ); - } - - Map> m = Map(); - for (final ep in details!.episodes!) { - var w = Container( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: 70, - child: Text("第 ${ep.episodeNumber} 集"), - ), - SizedBox( - width: 100, - child: Opacity( - opacity: 0.5, - child: Text("${ep.airDate}"), - ), - ), - Text("${ep.title}", textAlign: TextAlign.left), - const Expanded(child: Text("")), - IconButton( - onPressed: () { - _searchAndDownload( - context, seriesId, ep.seasonNumber!, ep.episodeNumber!); - }, - icon: const Icon(Icons.search)) - ], - ), - ); - if (m[ep.seasonNumber] == null) { - m[ep.seasonNumber!] = List.empty(growable: true); - } - m[ep.seasonNumber!]!.add(w); - } - List list = List.empty(growable: true); - for (final k in m.keys.toList().reversed) { - bool _customTileExpanded = false; - var seasonList = ExpansionTile( - tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0), - childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0), - initiallyExpanded: k == 0 ? false : true, - title: Text("第 $k 季"), - trailing: Icon( - _customTileExpanded - ? Icons.arrow_drop_down_circle - : Icons.arrow_drop_down, - ), - children: m[k]!, - onExpansionChanged: (bool expanded) { - setState(() { - _customTileExpanded = expanded; - }); - }, - ); - list.add(seasonList); - } - - return ListView( - children: [ - Card( - margin: const EdgeInsets.all(4), - clipBehavior: Clip.hardEdge, - child: Row( - children: [ - Flexible( - child: SizedBox( - width: 150, - height: 200, - child: Image.network( - APIs.tmdbImgBaseUrl + details!.posterPath!, - fit: BoxFit.contain, + var seriesDetails = ref.watch(seriesDetailsProvider(seriesId)); + return seriesDetails.when( + data: (details) { + Map> m = Map(); + for (final ep in details.episodes!) { + var w = Container( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: 70, + child: Text("第 ${ep.episodeNumber} 集"), ), - ), - ), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${details!.name}", - style: const TextStyle( - fontSize: 14, fontWeight: FontWeight.bold), + SizedBox( + width: 100, + child: Opacity( + opacity: 0.5, + child: Text("${ep.airDate}"), + ), + ), + Text("${ep.title}", textAlign: TextAlign.left), + const Expanded(child: Text("")), + IconButton( + onPressed: () { + _searchAndDownload(context, seriesId, ep.seasonNumber!, + ep.episodeNumber!); + }, + icon: const Icon(Icons.search)) + ], + ), + ); + if (m[ep.seasonNumber] == null) { + m[ep.seasonNumber!] = List.empty(growable: true); + } + m[ep.seasonNumber!]!.add(w); + } + List list = List.empty(growable: true); + for (final k in m.keys.toList().reversed) { + bool _customTileExpanded = false; + var seasonList = ExpansionTile( + tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0), + initiallyExpanded: k == 0 ? false : true, + title: Text("第 $k 季"), + trailing: Icon( + _customTileExpanded + ? Icons.arrow_drop_down_circle + : Icons.arrow_drop_down, + ), + children: m[k]!, + onExpansionChanged: (bool expanded) { + setState(() { + _customTileExpanded = expanded; + }); + }, + ); + list.add(seasonList); + } + return ListView( + children: [ + Card( + margin: const EdgeInsets.all(4), + clipBehavior: Clip.hardEdge, + child: Row( + children: [ + Flexible( + child: SizedBox( + width: 150, + height: 200, + child: Image.network( + APIs.tmdbImgBaseUrl + details!.posterPath!, + fit: BoxFit.contain, + ), + ), + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${details!.name}", + style: const TextStyle( + fontSize: 14, fontWeight: FontWeight.bold), + ), + const Text(""), + Text(details!.overview!) + ], + ), ), - const Text(""), - Text(details!.overview!) ], ), ), + Column( + children: list, + ), ], - ), - ), - Column( - children: list, - ), - - ], - ); - } - - void _querySeriesDetails() async { - if (details != null) { - return; - } - var resp = await Dio().get("${APIs.seriesDetailUrl}$seriesId"); - var rsp = ServerResponse.fromJson(resp.data); - - setState(() { - details = SeriesDetails.fromJson(rsp.data); - }); + ); + }, + error: (err, trace) { + return Text("$err"); + }, + loading: () => const CircularProgressIndicator()); } void _searchAndDownload(BuildContext context, String seriesId, int seasonNum, @@ -170,72 +157,3 @@ class _TvDetailsPageState extends State { } } } - -class SeriesDetails { - int? id; - int? tmdbId; - String? name; - String? originalName; - String? overview; - String? path; - String? posterPath; - String? createdAt; - List? episodes; - - SeriesDetails( - {this.id, - this.tmdbId, - this.name, - this.originalName, - this.overview, - this.path, - this.posterPath, - this.createdAt, - this.episodes}); - - SeriesDetails.fromJson(Map 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 = []; - 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 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']; - } -} diff --git a/ui/lib/weclome.dart b/ui/lib/weclome.dart index 113d518..7c8d179 100644 --- a/ui/lib/weclome.dart +++ b/ui/lib/weclome.dart @@ -1,37 +1,26 @@ -import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.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'; -class WelcomePage extends StatefulWidget { - const WelcomePage({super.key}); +class WelcomePage extends ConsumerWidget { static const route = "/welcome"; - @override - State createState() { - return _WeclomePageState(); - } -} - -class _WeclomePageState extends State { - var favList = List.empty(growable: true); + const WelcomePage({super.key}); @override - void initState() { - super.initState(); - _onRefresh(); - } + Widget build(BuildContext context, WidgetRef ref) { + final data = ref.watch(welcomePageDataProvider); - @override - Widget build(BuildContext context) { - return GridView.builder( - itemCount: favList.length, + return switch (data) { + AsyncData(:final value) => GridView.builder( + itemCount: value.length, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 6), itemBuilder: (context, i) { - var item = TvSeries.fromJson(favList[i]); + var item = value[i]; return Card( margin: const EdgeInsets.all(4), clipBehavior: Clip.hardEdge, @@ -63,46 +52,9 @@ class _WeclomePageState extends State { ], ), )); - }); - } - - Future _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 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"]; + }), + _ => const CircularProgressIndicator(), + + }; } } diff --git a/ui/pubspec.lock b/ui/pubspec.lock index 0301249..41a9d4c 100644 --- a/ui/pubspec.lock +++ b/ui/pubspec.lock @@ -78,6 +78,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted 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: dependency: "direct dev" description: flutter @@ -176,6 +184,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted 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: dependency: transitive description: flutter @@ -197,6 +213,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted 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: dependency: transitive description: diff --git a/ui/pubspec.yaml b/ui/pubspec.yaml index 0a865f2..661b282 100644 --- a/ui/pubspec.yaml +++ b/ui/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 go_router: ^14.2.0 + flutter_riverpod: ^2.5.1 dev_dependencies: flutter_test: