diff --git a/ui/lib/main.dart b/ui/lib/main.dart index 8af12ec..0314d7c 100644 --- a/ui/lib/main.dart +++ b/ui/lib/main.dart @@ -88,7 +88,7 @@ class MyApp extends StatelessWidget { return ProviderScope( child: MaterialApp.router( - title: 'Flutter Demo', + title: 'Polaris', theme: ThemeData( // This is the theme of your application. // diff --git a/ui/lib/APIs.dart b/ui/lib/providers/APIs.dart similarity index 100% rename from ui/lib/APIs.dart rename to ui/lib/providers/APIs.dart diff --git a/ui/lib/providers/series_details.dart b/ui/lib/providers/series_details.dart index 81b7df9..a19d8d4 100644 --- a/ui/lib/providers/series_details.dart +++ b/ui/lib/providers/series_details.dart @@ -1,16 +1,40 @@ +import 'dart:async'; + import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:ui/APIs.dart'; +import 'package:ui/providers/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; +var seriesDetailsProvider = AsyncNotifierProvider.autoDispose + .family(SeriesDetailData.new); + +class SeriesDetailData + extends AutoDisposeFamilyAsyncNotifier { + @override + FutureOr build(String arg) async { + var resp = await Dio().get("${APIs.seriesDetailUrl}$arg"); + var rsp = ServerResponse.fromJson(resp.data); + if (rsp.code != 0) { + throw rsp.message; + } + return SeriesDetails.fromJson(rsp.data); } - return SeriesDetails.fromJson(rsp.data); -}); + + Future searchAndDownload( + String seriesId, int seasonNum, int episodeNum) async { + var resp = await Dio().post(APIs.searchAndDownloadUrl, data: { + "id": int.parse(seriesId), + "season": seasonNum, + "episode": episodeNum, + }); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + var name = (sp.data as Map)["name"]; + return name; + } +} class SeriesDetails { int? id; diff --git a/ui/lib/providers/settings.dart b/ui/lib/providers/settings.dart new file mode 100644 index 0000000..66bfc82 --- /dev/null +++ b/ui/lib/providers/settings.dart @@ -0,0 +1,191 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quiver/strings.dart'; +import 'package:ui/providers/APIs.dart'; +import 'package:ui/server_response.dart'; + +var tmdbApiSettingProvider = + AsyncNotifierProvider(TmdbApiSetting.new); + +var indexersProvider = + AsyncNotifierProvider>(IndexerSetting.new); + +var dwonloadClientsProvider = + AsyncNotifierProvider>( + DownloadClientSetting.new); + +class TmdbApiSetting extends AsyncNotifier { + @override + FutureOr build() async { + final dio = Dio(); + var resp = await dio + .get(APIs.settingsUrl, queryParameters: {"key": APIs.tmdbApiKey}); + var rrr = ServerResponse.fromJson(resp.data); + if (rrr.code != 0) { + throw rrr.message; + } + var data = rrr.data as Map; + var key = data[APIs.tmdbApiKey] as String; + + return key; + } + + Future submitSettings(String v) async { + var resp = await Dio().post(APIs.settingsUrl, data: {APIs.tmdbApiKey: v}); + var sp = ServerResponse.fromJson(resp.data as Map); + if (sp.code != 0) { + throw sp.message; + } + } +} + +class IndexerSetting extends AsyncNotifier> { + final dio = Dio(); + + @override + FutureOr> build() async { + var resp = await dio.get(APIs.allIndexersUrl); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + List indexers = List.empty(growable: true); + for (final item in sp.data as List) { + indexers.add(Indexer.fromJson(item)); + } + return indexers; + } + + Future addIndexer(Indexer indexer) async { + if (isBlank(indexer.name) || + isBlank(indexer.url) || + isBlank(indexer.apiKey)) { + return; + } + var resp = await dio.post(APIs.addIndexerUrl, data: indexer.toJson()); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + ref.invalidateSelf(); + } + + Future deleteIndexer(int id) async { + var resp = await dio.delete("${APIs.delIndexerUrl}$id"); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + ref.invalidateSelf(); + } +} + +class Indexer { + String? name; + String? url; + String? apiKey; + int? id; + + Indexer({this.name, this.url, this.apiKey}); + + Indexer.fromJson(Map json) { + name = json['name']; + url = json['url']; + apiKey = json['api_key']; + id = json["id"]; + } + Map toJson() { + final Map data = new Map(); + data['name'] = this.name; + data['url'] = this.url; + data['api_key'] = this.apiKey; + return data; + } +} + +class DownloadClientSetting extends AsyncNotifier> { + final dio = Dio(); + + @override + FutureOr> build() async { + var resp = await dio.get(APIs.allDownloadClientsUrl); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + List indexers = List.empty(growable: true); + for (final item in sp.data as List) { + indexers.add(DownloadClient.fromJson(item)); + } + return indexers; + } + + Future addDownloadClients(String name, String url) async { + if (name.isEmpty || url.isEmpty) { + return; + } + var dio = Dio(); + var resp = await dio.post(APIs.addDownloadClientUrl, data: { + "name": name, + "url": url, + }); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + ref.invalidateSelf(); + } + + Future deleteDownloadClients(int id) async { + var dio = Dio(); + var resp = await dio.delete("${APIs.delDownloadClientUrl}$id"); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + ref.invalidateSelf(); + } +} + +class DownloadClient { + int? id; + bool? enable; + String? name; + String? implementation; + String? url; + bool? removeCompletedDownloads; + bool? removeFailedDownloads; + + DownloadClient( + {this.id, + this.enable, + this.name, + this.implementation, + this.url, + this.removeCompletedDownloads, + this.removeFailedDownloads}); + + DownloadClient.fromJson(Map json) { + id = json['id']; + enable = json['enable']; + name = json['name']; + implementation = json['implementation']; + url = json['url']; + removeCompletedDownloads = json['remove_completed_downloads']; + removeFailedDownloads = json['remove_failed_downloads']; + } + + Map toJson() { + final Map data = new Map(); + data['id'] = this.id; + data['enable'] = this.enable; + data['name'] = this.name; + data['implementation'] = this.implementation; + data['url'] = this.url; + data['remove_completed_downloads'] = this.removeCompletedDownloads; + data['remove_failed_downloads'] = this.removeFailedDownloads; + return data; + } +} diff --git a/ui/lib/providers/welcome_data.dart b/ui/lib/providers/welcome_data.dart index 1e0c9d7..b270ccb 100644 --- a/ui/lib/providers/welcome_data.dart +++ b/ui/lib/providers/welcome_data.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:ui/APIs.dart'; +import 'package:ui/providers/APIs.dart'; import 'package:ui/server_response.dart'; final welcomePageDataProvider = FutureProvider((ref) async { @@ -43,107 +43,6 @@ class TvSeries { } } -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; - }, -); -var indexersProvider = FutureProvider((ref) async { - final dio = Dio(); - var resp = await dio.get(APIs.allIndexersUrl); - var sp = ServerResponse.fromJson(resp.data); - if (sp.code != 0) { - throw sp.message; - } - List indexers = List.empty(growable: true); - for (final item in sp.data as List) { - indexers.add(Indexer.fromJson(item)); - } - return indexers; -}); -class Indexer { - String? name; - String? url; - String? apiKey; - int? id; - - Indexer({this.name, this.url, this.apiKey}); - - Indexer.fromJson(Map json) { - name = json['name']; - url = json['url']; - apiKey = json['api_key']; - id = json["id"]; - } - Map toJson() { - final Map data = new Map(); - data['name'] = this.name; - data['url'] = this.url; - data['api_key'] = this.apiKey; - return data; - } -} - -var dwonloadClientsProvider = FutureProvider((ref) async { - final dio = Dio(); - var resp = await dio.get(APIs.allDownloadClientsUrl); - var sp = ServerResponse.fromJson(resp.data); - if (sp.code != 0) { - throw sp.message; - } - List indexers = List.empty(growable: true); - for (final item in sp.data as List) { - indexers.add(DownloadClient.fromJson(item)); - } - return indexers; -}); - -class DownloadClient { - int? id; - bool? enable; - String? name; - String? implementation; - String? url; - bool? removeCompletedDownloads; - bool? removeFailedDownloads; - - DownloadClient( - {this.id, - this.enable, - this.name, - this.implementation, - this.url, - this.removeCompletedDownloads, - this.removeFailedDownloads}); - - DownloadClient.fromJson(Map json) { - id = json['id']; - enable = json['enable']; - name = json['name']; - implementation = json['implementation']; - url = json['url']; - removeCompletedDownloads = json['remove_completed_downloads']; - removeFailedDownloads = json['remove_failed_downloads']; - } - - Map toJson() { - final Map data = new Map(); - data['id'] = this.id; - data['enable'] = this.enable; - data['name'] = this.name; - data['implementation'] = this.implementation; - data['url'] = this.url; - data['remove_completed_downloads'] = this.removeCompletedDownloads; - data['remove_failed_downloads'] = this.removeFailedDownloads; - return data; - } -} diff --git a/ui/lib/search.dart b/ui/lib/search.dart index 1f8a213..959dc47 100644 --- a/ui/lib/search.dart +++ b/ui/lib/search.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:ui/APIs.dart'; +import 'package:ui/providers/APIs.dart'; import 'package:ui/providers/welcome_data.dart'; import 'package:ui/server_response.dart'; import 'package:ui/utils.dart'; diff --git a/ui/lib/system_settings.dart b/ui/lib/system_settings.dart index 2cf7077..0749660 100644 --- a/ui/lib/system_settings.dart +++ b/ui/lib/system_settings.dart @@ -1,9 +1,6 @@ -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/providers/settings.dart'; import 'package:ui/utils.dart'; class SystemSettingsPage extends ConsumerStatefulWidget { @@ -18,139 +15,160 @@ class SystemSettingsPage extends ConsumerStatefulWidget { class _SystemSettingsPageState extends ConsumerState { final GlobalKey _formKey = GlobalKey(); - - List indexers = List.empty(); - - @override - void initState() { - super.initState(); - } - + Future? _pendingTmdb; + Future? _pendingIndexer; + Future? _pendingDownloadClient; @override Widget build(BuildContext context) { var key = ref.watch(tmdbApiSettingProvider); - - var tmdbSetting = key.when( - data: (data) => Container( - padding: const EdgeInsets.fromLTRB(40, 10, 40, 0), - child: Form( - key: _formKey, //设置globalKey,用于后面获取FormState - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column( - children: [ - TextFormField( - autofocus: true, - initialValue: data, - decoration: const InputDecoration( - labelText: "TMDB Api Key", - icon: Icon(Icons.key), - ), - // - validator: (v) { - return v!.trim().isNotEmpty ? null : "ApiKey 不能为空"; - }, - onSaved: (newValue) { - _submitSettings(context, newValue!); - }, - ), - Center( - child: Padding( - padding: const EdgeInsets.only(top: 28.0), - child: ElevatedButton( - child: const Padding( - padding: EdgeInsets.all(16.0), - child: Text("保存"), + var tmdbSetting = FutureBuilder( + // We listen to the pending operation, to update the UI accordingly. + future: _pendingTmdb, + builder: (context, snapshot) { + return key.when( + data: (value) => Container( + padding: const EdgeInsets.fromLTRB(40, 10, 40, 0), + child: Form( + key: _formKey, //设置globalKey,用于后面获取FormState + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + children: [ + TextFormField( + autofocus: true, + initialValue: value, + decoration: const InputDecoration( + labelText: "TMDB Api Key", + icon: Icon(Icons.key), + ), + // + validator: (v) { + return v!.trim().isNotEmpty + ? null + : "ApiKey 不能为空"; + }, + onSaved: (newValue) { + var furture = ref + .read(tmdbApiSettingProvider.notifier) + .submitSettings(newValue!); + setState(() { + _pendingTmdb = furture; + }); + if (!showError(snapshot)) { + Navigator.of(context).pop(); + } + }, ), - onPressed: () { - // 通过_formKey.currentState 获取FormState后, - // 调用validate()方法校验用户名密码是否合法,校验 - // 通过后再提交数据。 - if ((_formKey.currentState as FormState) - .validate()) { - (_formKey.currentState as FormState).save(); - } - }, - ), + Center( + child: Padding( + padding: const EdgeInsets.only(top: 28.0), + child: ElevatedButton( + child: const Padding( + padding: EdgeInsets.all(16.0), + child: Text("保存"), + ), + onPressed: () { + // 通过_formKey.currentState 获取FormState后, + // 调用validate()方法校验用户名密码是否合法,校验 + // 通过后再提交数据。 + if ((_formKey.currentState as FormState) + .validate()) { + (_formKey.currentState as FormState).save(); + } + }, + ), + ), + ) + ], ), - ) - ], - ), - ), - ), - error: (err, trace) => Text("$err"), - loading: () => const CircularProgressIndicator()); + ), + ), + error: (err, trace) => Text("$err"), + loading: () => const CircularProgressIndicator()); + }); var indexers = ref.watch(indexersProvider); - var indexerSetting = indexers.when( - data: (value) => GridView.builder( - itemCount: value.length + 1, - scrollDirection: Axis.vertical, - shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 6), - itemBuilder: (context, i) { - if (i < value.length) { - var indexer = value[i]; - return Card( - margin: const EdgeInsets.all(4), - clipBehavior: Clip.hardEdge, - child: InkWell( - //splashColor: Colors.blue.withAlpha(30), - onTap: () { - showIndexerDetails(context, indexer); - }, - child: Center(child: Text(indexer.name!)))); - } - return Card( - margin: const EdgeInsets.all(4), - clipBehavior: Clip.hardEdge, - child: InkWell( - //splashColor: Colors.blue.withAlpha(30), - onTap: () { - showIndexerDetails(context, Indexer()); - }, - child: const Center( - child: Icon(Icons.add), - ))); - }), - error: (err, trace) => Text("$err"), - loading: () => const CircularProgressIndicator()); + var indexerSetting = FutureBuilder( + // We listen to the pending operation, to update the UI accordingly. + future: _pendingIndexer, + builder: (context, snapshot) { + return indexers.when( + data: (value) => GridView.builder( + itemCount: value.length + 1, + scrollDirection: Axis.vertical, + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 6), + itemBuilder: (context, i) { + if (i < value.length) { + var indexer = value[i]; + return Card( + margin: const EdgeInsets.all(4), + clipBehavior: Clip.hardEdge, + child: InkWell( + //splashColor: Colors.blue.withAlpha(30), + onTap: () { + showIndexerDetails(snapshot, context, indexer); + }, + child: Center(child: Text(indexer.name!)))); + } + return Card( + margin: const EdgeInsets.all(4), + clipBehavior: Clip.hardEdge, + child: InkWell( + //splashColor: Colors.blue.withAlpha(30), + onTap: () { + showIndexerDetails(snapshot, context, Indexer()); + }, + child: const Center( + child: Icon(Icons.add), + ))); + }), + error: (err, trace) => Text("$err"), + loading: () => const CircularProgressIndicator()); + }); var downloadClients = ref.watch(dwonloadClientsProvider); - var downloadSetting = downloadClients.when( - data: (value) => GridView.builder( - itemCount: value.length + 1, - scrollDirection: Axis.vertical, - shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 6), - itemBuilder: (context, i) { - if (i < value.length) { - var client = value[i]; - return Card( - margin: const EdgeInsets.all(4), - clipBehavior: Clip.hardEdge, - child: InkWell( - //splashColor: Colors.blue.withAlpha(30), - onTap: () { - showDownloadClientDetails(context, client); - }, - child: Center(child: Text(client.name!)))); - } - return Card( - margin: const EdgeInsets.all(4), - clipBehavior: Clip.hardEdge, - child: InkWell( - //splashColor: Colors.blue.withAlpha(30), - onTap: () { - showDownloadClientDetails(context, DownloadClient()); - }, - child: const Center( - child: Icon(Icons.add), - ))); - }), - error: (err, trace) => Text("$err"), - loading: () => const CircularProgressIndicator()); + var downloadSetting = FutureBuilder( + // We listen to the pending operation, to update the UI accordingly. + future: _pendingDownloadClient, + builder: (context, snapshot) { + return downloadClients.when( + data: (value) => GridView.builder( + itemCount: value.length + 1, + scrollDirection: Axis.vertical, + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 6), + itemBuilder: (context, i) { + if (i < value.length) { + var client = value[i]; + return Card( + margin: const EdgeInsets.all(4), + clipBehavior: Clip.hardEdge, + child: InkWell( + //splashColor: Colors.blue.withAlpha(30), + onTap: () { + showDownloadClientDetails( + snapshot, context, client); + }, + child: Center(child: Text(client.name!)))); + } + return Card( + margin: const EdgeInsets.all(4), + clipBehavior: Clip.hardEdge, + child: InkWell( + //splashColor: Colors.blue.withAlpha(30), + onTap: () { + showDownloadClientDetails( + snapshot, context, DownloadClient()); + }, + child: const Center( + child: Icon(Icons.add), + ))); + }), + error: (err, trace) => Text("$err"), + loading: () => const CircularProgressIndicator()); + }); return ListView( children: [ @@ -179,17 +197,8 @@ class _SystemSettingsPageState extends ConsumerState { ); } - void _submitSettings(BuildContext context, String v) async { - var resp = await Dio().post(APIs.settingsUrl, data: {APIs.tmdbApiKey: v}); - var sp = ServerResponse.fromJson(resp.data as Map); - if (sp.code != 0) { - if (context.mounted) { - Utils.showAlertDialog(context, sp.message); - } - } - } - - Future showIndexerDetails(BuildContext context, Indexer indexer) { + Future showIndexerDetails( + AsyncSnapshot snapshot, BuildContext context, Indexer indexer) { var nameController = TextEditingController(text: indexer.name); var urlController = TextEditingController(text: indexer.url); var apiKeyController = TextEditingController(text: indexer.apiKey); @@ -219,9 +228,19 @@ class _SystemSettingsPageState extends ConsumerState { ), actions: [ indexer.id == null - ? Text("") + ? const Text("") : TextButton( - onPressed: () => {deleteIndexer(context, indexer.id!)}, + onPressed: () { + var f = ref + .read(indexersProvider.notifier) + .deleteIndexer(indexer.id!); + setState(() { + _pendingIndexer = f; + }); + if (!showError(snapshot)) { + Navigator.of(context).pop(); + } + }, child: const Text('删除')), TextButton( onPressed: () => Navigator.of(context).pop(), @@ -229,8 +248,18 @@ class _SystemSettingsPageState extends ConsumerState { TextButton( child: const Text('确定'), onPressed: () { - addIndexer(context, nameController.text, urlController.text, - apiKeyController.text); + var f = ref.read(indexersProvider.notifier).addIndexer( + Indexer( + name: nameController.text, + url: urlController.text, + apiKey: apiKeyController.text)); + setState(() { + _pendingIndexer = f; + }); + + if (!showError(snapshot)) { + Navigator.of(context).pop(); + } }, ), ], @@ -238,36 +267,7 @@ class _SystemSettingsPageState extends ConsumerState { }); } - void addIndexer( - BuildContext context, String name, String url, String apiKey) async { - if (name.isEmpty || url.isEmpty || apiKey.isEmpty) { - return; - } - var dio = Dio(); - var resp = await dio.post(APIs.addIndexerUrl, - data: Indexer(name: name, url: url, apiKey: apiKey).toJson()); - var sp = ServerResponse.fromJson(resp.data); - if (sp.code != 0 && context.mounted) { - Utils.showAlertDialog(context, sp.message); - return; - } - Navigator.of(context).pop(); - ref.refresh(indexersProvider); - } - - void deleteIndexer(BuildContext context, int id) async { - var dio = Dio(); - var resp = await dio.delete("${APIs.delIndexerUrl}$id"); - var sp = ServerResponse.fromJson(resp.data); - if (sp.code != 0 && context.mounted) { - Utils.showAlertDialog(context, sp.message); - return; - } - Navigator.of(context).pop(); - ref.refresh(indexersProvider); - } - - Future showDownloadClientDetails( + Future showDownloadClientDetails(AsyncSnapshot snapshot, BuildContext context, DownloadClient client) { var nameController = TextEditingController(text: client.name); var urlController = TextEditingController(text: client.url); @@ -294,10 +294,19 @@ class _SystemSettingsPageState extends ConsumerState { ), actions: [ client.id == null - ? Text("") + ? const Text("") : TextButton( - onPressed: () => - {deleteDownloadClients(context, client.id!)}, + onPressed: () { + var f = ref + .read(dwonloadClientsProvider.notifier) + .deleteDownloadClients(client.id!); + setState(() { + _pendingDownloadClient = f; + }); + if (!showError(snapshot)) { + Navigator.of(context).pop(); + } + }, child: const Text('删除')), TextButton( onPressed: () => Navigator.of(context).pop(), @@ -305,8 +314,16 @@ class _SystemSettingsPageState extends ConsumerState { TextButton( child: const Text('确定'), onPressed: () { - addDownloadClients( - context, nameController.text, urlController.text); + var f = ref + .read(dwonloadClientsProvider.notifier) + .addDownloadClients( + nameController.text, urlController.text); + setState(() { + _pendingDownloadClient = f; + }); + if (!showError(snapshot)) { + Navigator.of(context).pop(); + } }, ), ], @@ -314,33 +331,13 @@ class _SystemSettingsPageState extends ConsumerState { }); } - void addDownloadClients(BuildContext context, String name, String url) async { - if (name.isEmpty || url.isEmpty) { - return; + bool showError(AsyncSnapshot snapshot) { + final isErrored = snapshot.hasError && + snapshot.connectionState != ConnectionState.waiting; + if (isErrored) { + Utils.showSnakeBar(context, "当前操作出错: ${snapshot.error}"); + return true; } - var dio = Dio(); - var resp = await dio.post(APIs.addDownloadClientUrl, data: { - "name": name, - "url": url, - }); - var sp = ServerResponse.fromJson(resp.data); - if (sp.code != 0 && context.mounted) { - Utils.showAlertDialog(context, sp.message); - return; - } - Navigator.of(context).pop(); - ref.refresh(dwonloadClientsProvider); - } - - void deleteDownloadClients(BuildContext context, int id) async { - var dio = Dio(); - var resp = await dio.delete("${APIs.delDownloadClientUrl}$id"); - var sp = ServerResponse.fromJson(resp.data); - if (sp.code != 0 && context.mounted) { - Utils.showAlertDialog(context, sp.message); - return; - } - Navigator.of(context).pop(); - ref.refresh(dwonloadClientsProvider); + return false; } } diff --git a/ui/lib/tv_details.dart b/ui/lib/tv_details.dart index 19f55e7..6a00a94 100644 --- a/ui/lib/tv_details.dart +++ b/ui/lib/tv_details.dart @@ -1,9 +1,7 @@ -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/APIs.dart'; import 'package:ui/providers/series_details.dart'; -import 'package:ui/server_response.dart'; import 'package:ui/utils.dart'; class TvDetailsPage extends ConsumerStatefulWidget { @@ -27,6 +25,7 @@ class _TvDetailsPageState extends ConsumerState { final String seriesId; _TvDetailsPageState({required this.seriesId}); + Future? _pendingFuture; @override void initState() { @@ -36,113 +35,112 @@ class _TvDetailsPageState extends ConsumerState { @override Widget build(BuildContext context) { 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} 集"), - ), - 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) { - 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 季"), - children: m[k]!, - ); - 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, + return FutureBuilder( + // We listen to the pending operation, to update the UI accordingly. + future: _pendingFuture, + builder: (context, snapshot) { + 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: () async { + var f = ref + .read( + seriesDetailsProvider(seriesId).notifier) + .searchAndDownload(seriesId, ep.seasonNumber!, + ep.episodeNumber!); + setState(() { + _pendingFuture = f; + }); + if (!Utils.showError(context, snapshot)) { + var name = await f; + Utils.showSnakeBar( + context, "开始下载: $name"); + } + }, + 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) { + 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 季"), + children: m[k]!, + ); + 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, - ), - ], - ); - }, - error: (err, trace) { - return Text("$err"); - }, - loading: () => const CircularProgressIndicator()); - } - - void _searchAndDownload(BuildContext context, String seriesId, int seasonNum, - int episodeNum) async { - var resp = await Dio().post(APIs.searchAndDownloadUrl, data: { - "id": int.parse(seriesId), - "season": seasonNum, - "episode": episodeNum, - }); - var sp = ServerResponse.fromJson(resp.data); - if (sp.code != 0 && context.mounted) { - Utils.showAlertDialog(context, sp.message); - return; - } - var name = (sp.data as Map)["name"]; - if (context.mounted) { - Utils.showSnakeBar(context, "$name 开始下载..."); - } + ); + }, + error: (err, trace) { + return Text("$err"); + }, + loading: () => const CircularProgressIndicator()); + }); } } diff --git a/ui/lib/utils.dart b/ui/lib/utils.dart index b4c018d..50d42e4 100644 --- a/ui/lib/utils.dart +++ b/ui/lib/utils.dart @@ -31,4 +31,14 @@ class Utils { static showSnakeBar(BuildContext context, String msg) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); } + + static bool showError(BuildContext context, AsyncSnapshot snapshot) { + final isErrored = snapshot.hasError && + snapshot.connectionState != ConnectionState.waiting; + if (isErrored) { + Utils.showSnakeBar(context, "当前操作出错: ${snapshot.error}"); + return true; + } + return false; + } } diff --git a/ui/lib/weclome.dart b/ui/lib/weclome.dart index 1aadfa0..cbe39bb 100644 --- a/ui/lib/weclome.dart +++ b/ui/lib/weclome.dart @@ -1,7 +1,7 @@ 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/providers/APIs.dart'; import 'package:ui/providers/welcome_data.dart'; import 'package:ui/tv_details.dart'; diff --git a/ui/pubspec.lock b/ui/pubspec.lock index 41a9d4c..6f338b0 100644 --- a/ui/pubspec.lock +++ b/ui/pubspec.lock @@ -184,6 +184,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.9.0" + quiver: + dependency: "direct main" + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" riverpod: dependency: transitive description: diff --git a/ui/pubspec.yaml b/ui/pubspec.yaml index 661b282..e6e25d2 100644 --- a/ui/pubspec.yaml +++ b/ui/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: cupertino_icons: ^1.0.6 go_router: ^14.2.0 flutter_riverpod: ^2.5.1 + quiver: ^3.2.1 dev_dependencies: flutter_test: