diff --git a/lib/models/refresh.dart b/lib/models/refresh.dart deleted file mode 100644 index ca52587..0000000 --- a/lib/models/refresh.dart +++ /dev/null @@ -1,20 +0,0 @@ -/// 刷新令牌 -class Refresh { - String token; - DateTime expire; - - Refresh({ - required this.token, - required this.expire, - }); - - factory Refresh.fromJson(Map json) => Refresh( - token: json['token'] as String, - expire: DateTime.parse(json['expire'] as String), - ); - - Map toJson() => { - 'token': token, - 'expire': expire.toIso8601String(), - }; -} \ No newline at end of file diff --git a/lib/provider/follow_provider.dart b/lib/provider/follow_provider.dart index 4d6eb2c..2c46104 100644 --- a/lib/provider/follow_provider.dart +++ b/lib/provider/follow_provider.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:komikku/dex/apis/manga_api.dart'; -import 'package:komikku/utils/user.dart'; +import 'package:komikku/utils/auth.dart'; class FollowProvider extends ChangeNotifier { /// 订阅漫画 Future followManga(String id) async { // 未登录,直接返回 - if (!await userLoginState()) return; + if (!await Auth.userLoginState) return; await MangaApi.followMangaAsync(id); notifyListeners(); @@ -15,7 +15,7 @@ class FollowProvider extends ChangeNotifier { /// 退订漫画 Future unfollowManga(String id) async { // 未登录,直接返回 - if (!await userLoginState()) return; + if (!await Auth.userLoginState) return; await MangaApi.unfollowMangaAsync(id); notifyListeners(); diff --git a/lib/provider/user_provider.dart b/lib/provider/user_provider.dart index 39b7761..9b40553 100644 --- a/lib/provider/user_provider.dart +++ b/lib/provider/user_provider.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:komikku/dex/apis/auth_api.dart'; import 'package:komikku/dex/models/login.dart'; -import 'package:komikku/utils/user.dart'; +import 'package:komikku/utils/auth.dart'; class UserProvider extends ChangeNotifier { /// 登录 @@ -11,16 +11,16 @@ class UserProvider extends ChangeNotifier { : Login(username: emailOrUsername, password: password); var response = await AuthApi.loginAsync(login); - await setRefresh(response.token.refresh); - await setSession(response.token.session); + await Auth.setRefresh(response.token.refresh); + await Auth.setSession(response.token.session); notifyListeners(); } /// 登出 Future logout() async { - await removeSession(); - await removeRefresh(); + await Auth.removeSession(); + await Auth.removeRefresh(); await AuthApi.logoutAsync(); notifyListeners(); diff --git a/lib/utils/auth.dart b/lib/utils/auth.dart new file mode 100644 index 0000000..e6e693c --- /dev/null +++ b/lib/utils/auth.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class Auth { + /// 用户状态 + static Future get userLoginState async { + return await session != null || await refresh != null; + } + + /// 获取会话令牌 + static Future get session async { + final prefs = await SharedPreferences.getInstance(); + final list = prefs.getStringList('session'); + if (list == null) return null; + + if (DateTime.now().isAfter(DateTime.parse(list[0]))) { + await prefs.remove('session'); + return null; + } + + return list[1]; + } + + /// 设置会话令牌 + static Future setSession(String token) async { + final prefs = await SharedPreferences.getInstance(); + + var expire = DateTime.now().add(const Duration(minutes: 14)).toIso8601String(); + return await prefs.setStringList('session', [expire, token]); + } + + /// 移除会话令牌 + static Future removeSession() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('session'); + } + + /// 获取刷新令牌 + static Future get refresh async { + var file = await _file; + if (!await file.exists()) return null; + + var refreshMap = json.decode(await file.readAsString()); + // 过期返回null + if (DateTime.now().isAfter(DateTime.parse(refreshMap['expire']))) { + return null; + } + + return refreshMap['token']; + } + + /// 设置刷新令牌 + static Future setRefresh(String token) async { + var file = await _file; + var expire = DateTime.now().add(const Duration(days: 29)).toIso8601String(); + await file.writeAsString(json.encode({'token': token, 'expire': expire})); + } + + /// 移除会话令牌 + static Future removeRefresh() async { + var file = await _file; + if (await file.exists()) await file.delete(); + } + + /// 获取 .token 文件 + static Future get _file async { + final directory = await getApplicationDocumentsDirectory(); + return File('${directory.path}/.token'); + } +} diff --git a/lib/utils/http.dart b/lib/utils/http.dart index f0cedaa..2a4e1c6 100644 --- a/lib/utils/http.dart +++ b/lib/utils/http.dart @@ -2,7 +2,7 @@ import 'package:dio/dio.dart'; import 'package:komikku/dex/apis/auth_api.dart'; import 'package:komikku/dex/settings.dart'; import 'package:komikku/dex/models/refresh_token.dart'; -import 'package:komikku/utils/user.dart'; +import 'package:komikku/utils/auth.dart'; class HttpUtil { static final HttpUtil _instance = HttpUtil._internal(); @@ -93,14 +93,14 @@ class HttpUtil { Options options = Options(); // 查看是否登录 - if (await userLoginState()) { - var token = await getSession(); + if (await Auth.userLoginState) { + var token = await Auth.session; // session 为空的情况,请求刷新session if (token == null) { - var response = await AuthApi.refreshAsync(RefreshToken(token: (await getRefresh())!)); + var response = await AuthApi.refreshAsync(RefreshToken(token: (await Auth.refresh)!)); token = response.token.session; - await setSession(token); + await Auth.setSession(token); } options = Options(headers: { diff --git a/lib/utils/local_setting.dart b/lib/utils/local_setting.dart new file mode 100644 index 0000000..c9dd41a --- /dev/null +++ b/lib/utils/local_setting.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path_provider/path_provider.dart'; + +/// 本地设置 +class LocalSetting { + /// 查询 .setting 文件 + static Future> all() async { + var file = await _file; + return json.decode(await file.readAsString()); + } + + /// 往 .setting 文件中插入数据 + static Future insert(Map map) async { + var file = await _file; + var oldMap = await json.decode(await file.readAsString()) as Map; + + for (var key in map.keys) { + if (oldMap.containsKey(key)) { + oldMap[key] = map[key]; + } else { + oldMap.addAll({key: map[key]}); + } + } + + file.writeAsString(json.encode(oldMap)); + } + + /// 从 .setting 文件中删除数据 + static Future delete(key) async { + var file = await _file; + var oldMap = await json.decode(await file.readAsString()) as Map; + + if (oldMap.containsKey(key)) { + oldMap.remove(key); + } + + file.writeAsString(json.encode(oldMap)); + } + + /// 获取 .setting 文件 + static Future get _file async { + final directory = await getApplicationDocumentsDirectory(); + var file = File('${directory.path}/.setting'); + // 不存在则使用默认项新建 + if (!await file.exists()) file.writeAsString(json.encode(_default)); + + return file; + } + + /// 默认 .user 文件 + static const _default = { + // 内容分级 + 'content_rating': ['safe', 'suggestive', 'erotica', 'pornographic'], + // 章节可用翻译语言 + 'translated_language': ['zh', 'zh-hk'], + // 阅读图片是否压缩 + 'data_saver': false, + }; +} + +/// 获取内容分级 +Future> getContentRating() async { + var all = await LocalSetting.all(); + return all['content_rating']; +} + +/// 设置内容分级 +Future setContentRating(List contentRating) async => + await LocalSetting.insert({'content_rating': contentRating}); diff --git a/lib/utils/user.dart b/lib/utils/user.dart deleted file mode 100644 index 8d648fa..0000000 --- a/lib/utils/user.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:komikku/models/refresh.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -/// 刷新令牌过期时间 -const _refreshExpire = Duration(days: 29); - -/// 会话令牌过期时间 -const _sessionExpire = Duration(minutes: 14); - -/// 用户状态 -Future userLoginState() async { - return await getSession() != null || await getRefresh() != null; -} - -/// 获取会话令牌 -Future getSession() async { - final prefs = await SharedPreferences.getInstance(); - final list = prefs.getStringList('session'); - if (list == null) return null; - - if (DateTime.now().isAfter(DateTime.parse(list[0]))) { - await prefs.remove('session'); - return null; - } - - return list[1]; -} - -/// 获取刷新令牌 -Future getRefresh() async { - var file = await _localFile; - // 不存在时返回 null - if (!await file.exists()) return null; - - var jsonString = await file.readAsString(); - if (jsonString.isEmpty) return null; - - var refreshMap = jsonDecode(jsonString); - // 过期返回null - if (DateTime.now().isAfter(DateTime.parse(refreshMap['expire']))) { - return null; - } - - return refreshMap['token']; -} - -/// 设置会话令牌 -Future setSession(String token) async { - final prefs = await SharedPreferences.getInstance(); - var result = await prefs.setStringList('session', [ - DateTime.now().add(_sessionExpire).toIso8601String(), - token, - ]); - - return result; -} - -/// 设置刷新令牌 -Future setRefresh(String token) async { - var file = await _localFile; - var jsonString = jsonEncode(Refresh( - token: token, - expire: DateTime.now().add(_refreshExpire), - )); - - file.writeAsString(jsonString); -} - -/// 移除会话令牌 -Future removeSession() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.remove('session'); -} - -/// 移除会话令牌 -Future removeRefresh() async { - var file = await _localFile; - if (await file.exists()) file.delete(); -} - -/// 本地文件 -Future get _localFile async { - final path = await _localPath; - return File('$path/refresh_token'); -} - -/// 应用目录 -Future get _localPath async { - final directory = await getApplicationDocumentsDirectory(); - return directory.path; -} diff --git a/lib/views/details.dart b/lib/views/details.dart index 8143423..22f3877 100644 --- a/lib/views/details.dart +++ b/lib/views/details.dart @@ -3,12 +3,13 @@ import 'package:flutter/material.dart'; import 'package:komikku/dex/apis.dart'; import 'package:komikku/dex/apis/follows_api.dart'; import 'package:komikku/dex/models.dart'; +import 'package:komikku/dex/models/chapter_list.dart'; import 'package:komikku/dex/models/query/manga_feed_query.dart'; import 'package:komikku/dto/chapter_dto.dart'; import 'package:komikku/dto/manga_dto.dart'; import 'package:komikku/provider/follow_provider.dart'; import 'package:komikku/utils/timeago.dart'; -import 'package:komikku/utils/user.dart'; +import 'package:komikku/utils/auth.dart'; import 'package:komikku/views/reading.dart'; import 'package:komikku/widgets/bottom_modal_item.dart'; import 'package:komikku/widgets/builder_checker.dart'; @@ -27,12 +28,13 @@ class Details extends StatefulWidget { } class _DetailsState extends State
{ - final _followIconValueNotifier = ValueNotifier(false); - final _chapterListValueNotifier = ValueNotifier(false); - var _orderMode = OrderMode.desc; - late final Future> _chapterListFuture = _getMangaFeed(); + final _followIconValueNotifier = ValueNotifier(false); + final _orderValueNotifier = ValueNotifier(OrderMode.desc); - // 是否正在执行关键任务 + /// 因为需要排序,所以将响应内容缓存 + Future? _getMangaFeedFuture; + + /// 是否正在执行关键任务 bool _isBusy = false; @override @@ -40,7 +42,7 @@ class _DetailsState extends State
{ var followProvider = Provider.of(context, listen: false); return DelayPop( - flag: _isBusy, + flag: () => _isBusy, duration: const Duration(seconds: 1), child: Scaffold( extendBodyBehindAppBar: true, @@ -101,19 +103,19 @@ class _DetailsState extends State
{ ), onPressed: () async { // 未登录 - if (!await userLoginState()) { + if (!await Auth.userLoginState) { showText(text: '请先登录'); return; } // 已登录 - _isBusy = true; if (_followIconValueNotifier.value) { // 取消订阅确认 showAlertDialog( title: '是否取消订阅', cancelText: '再想想', onConfirm: () async { + _isBusy = true; showText(text: '已取消订阅'); _followIconValueNotifier.value = !_followIconValueNotifier.value; await followProvider.unfollowManga(widget.dto.id); @@ -122,6 +124,7 @@ class _DetailsState extends State
{ ); } else { // 订阅 + _isBusy = true; showText(text: '已订阅'); _followIconValueNotifier.value = !_followIconValueNotifier.value; await followProvider.followManga(widget.dto.id); @@ -175,7 +178,7 @@ class _DetailsState extends State
{ child: Directionality( textDirection: TextDirection.rtl, child: ValueListenableBuilder( - valueListenable: _chapterListValueNotifier, + valueListenable: _orderValueNotifier, builder: (context, value, child) => TextButton.icon( style: ButtonStyle( padding: MaterialStateProperty.all(const EdgeInsets.all(0)), @@ -183,15 +186,15 @@ class _DetailsState extends State
{ foregroundColor: MaterialStateProperty.all(Colors.black54), ), label: const Text('排序'), - icon: _chapterListValueNotifier.value + icon: _orderValueNotifier.value == OrderMode.asc ? const Icon(Icons.arrow_upward_rounded, size: 18) : const Icon(Icons.arrow_downward_rounded, size: 18), onPressed: () { - _orderMode == OrderMode.desc - ? _orderMode = OrderMode.asc - : _orderMode = OrderMode.desc; - - _chapterListValueNotifier.value = !_chapterListValueNotifier.value; + if (_orderValueNotifier.value == OrderMode.desc) { + _orderValueNotifier.value = OrderMode.asc; + } else { + _orderValueNotifier.value = OrderMode.desc; + } }, ), ), @@ -201,9 +204,9 @@ class _DetailsState extends State
{ // 内容 ValueListenableBuilder( - valueListenable: _chapterListValueNotifier, + valueListenable: _orderValueNotifier, builder: (context, value, child) => FutureBuilder>( - future: _chapterListFuture, + future: _getMangaFeed(), builder: (context, snapshot) => BuilderChecker( snapshot: snapshot, waiting: const Center( @@ -225,7 +228,7 @@ class _DetailsState extends State
{ /// 获取漫画章节 Future> _getMangaFeed() async { - var response = await MangaApi.getMangaFeedAsync(widget.dto.id, + var response = await (_getMangaFeedFuture ??= MangaApi.getMangaFeedAsync(widget.dto.id, query: MangaFeedQuery( limit: 96, offset: 0, @@ -239,35 +242,46 @@ class _DetailsState extends State
{ ], ), // 切勿 readableAt: OrderMode.desc, 否则缺少章节 - order: MangaFeedOrder(volume: OrderMode.desc, chapter: OrderMode.desc)); + order: MangaFeedOrder(volume: OrderMode.desc, chapter: OrderMode.desc))); var newItems = response.data.map((e) => ChapterDto.fromDex(e)).toList(); - // 按章节排序 - if (_orderMode == OrderMode.desc) { - if (newItems - .any((value) => value.chapter != null && double.tryParse(value.chapter!) != null)) { + if (!newItems + .any((value) => value.chapter == null || double.tryParse(value.chapter!) == null)) { + // 按章节排序 + if (_orderValueNotifier.value == OrderMode.desc) { newItems.sortByCompare( (value) => double.parse(value.chapter!), (double a, double b) => b.compareTo(a), ); - } - } else { - if (newItems - .any((value) => value.chapter != null && double.tryParse(value.chapter!) != null)) { + } else { newItems.sortByCompare( (value) => double.parse(value.chapter!), (double a, double b) => a.compareTo(b), ); } + } else { + // 没有章节,按readableAt排序 + if (_orderValueNotifier.value == OrderMode.desc) { + newItems.sortByCompare( + (value) => value.readableAt, + (DateTime a, DateTime b) => b.compareTo(a), + ); + } else { + newItems.sortByCompare( + (value) => value.readableAt, + (DateTime a, DateTime b) => a.compareTo(b), + ); + } } + return newItems; } /// 检测漫画是否被订阅 Future _checkUserFollow() async { // 未登录,直接返回false - if (!await userLoginState()) { + if (!await Auth.userLoginState) { return false; } diff --git a/lib/views/me.dart b/lib/views/me.dart index 199d162..a6aa5db 100644 --- a/lib/views/me.dart +++ b/lib/views/me.dart @@ -5,7 +5,7 @@ import 'package:komikku/dex/apis/user_api.dart'; import 'package:komikku/provider/user_provider.dart'; import 'package:komikku/utils/icons.dart'; import 'package:komikku/utils/toast.dart'; -import 'package:komikku/utils/user.dart'; +import 'package:komikku/utils/auth.dart'; import 'package:komikku/widgets/builder_checker.dart'; import 'package:komikku/widgets/icon_text_button.dart'; import 'package:provider/provider.dart'; @@ -51,7 +51,7 @@ class _MeState extends State { ), Consumer( builder: (context, userProvider, child) => FutureBuilder( - future: userLoginState(), + future: Auth.userLoginState, builder: (context, snapshot) => BuilderChecker( snapshot: snapshot, builder: (context) => OutlinedButton( @@ -95,7 +95,7 @@ class _MeState extends State { padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), child: Consumer( builder: (context, userProvider, child) => FutureBuilder( - future: userLoginState(), + future: Auth.userLoginState, builder: (context, snapshot) => BuilderChecker( snapshot: snapshot, builder: (context) { @@ -137,7 +137,7 @@ class _MeState extends State { /// 获取用户信息 Future _getUserDetails() async { - if (!await userLoginState()) throw Exception('Invalid operation'); + if (!await Auth.userLoginState) throw Exception('Invalid operation'); var response = await UserApi.getUserDetailsAsync(); return response.data.attributes.username; diff --git a/lib/views/subscribes.dart b/lib/views/subscribes.dart index 6a79ffa..d04afa1 100644 --- a/lib/views/subscribes.dart +++ b/lib/views/subscribes.dart @@ -7,7 +7,7 @@ import 'package:komikku/dex/models/query/usual_query.dart'; import 'package:komikku/dto/manga_dto.dart'; import 'package:komikku/provider/follow_provider.dart'; import 'package:komikku/provider/user_provider.dart'; -import 'package:komikku/utils/user.dart'; +import 'package:komikku/utils/auth.dart'; import 'package:komikku/views/details.dart'; import 'package:komikku/widgets/builder_checker.dart'; import 'package:komikku/widgets/grid_view_item.dart'; @@ -83,7 +83,7 @@ class _SubscribesState extends State { // 遮盖的内容(处于上层) Consumer2( builder: (context, userProvider, followProvider, child) => FutureBuilder( - future: userLoginState(), + future: Auth.userLoginState, builder: (context, snapshot) { return BuilderChecker( snapshot: snapshot, @@ -96,7 +96,10 @@ class _SubscribesState extends State { ); } - _pagingController.refresh(); + // 延后1秒钟执行refresh() + var delay = const Duration(seconds: 1); + (() async => await Future.delayed(delay, () => _pagingController.refresh()))(); + return const SizedBox.shrink(); }, ); @@ -110,6 +113,11 @@ class _SubscribesState extends State { /// 获取用户订阅的漫画 Future _getUserFollowedMangaList(int pageKey) async { + if (!await Auth.userLoginState) { + _pagingController.appendPage([], 0); + return; + } + var response = await FollowsApi.getUserFollowedMangaListAsync( query: UsualQuery(limit: _pageSize, offset: pageKey, includes: ['cover_art', 'author']), ); diff --git a/lib/widgets/delay_pop.dart b/lib/widgets/delay_pop.dart index 804d94f..05b0302 100644 --- a/lib/widgets/delay_pop.dart +++ b/lib/widgets/delay_pop.dart @@ -8,7 +8,7 @@ class DelayPop extends StatelessWidget { required this.child, }) : super(key: key); - final bool flag; + final bool Function() flag; final Duration duration; final Widget child; @@ -18,7 +18,7 @@ class DelayPop extends StatelessWidget { child: child, onWillPop: () async { // 如果正在执行关键任务,则等待再返回 - if (flag) await Future.delayed(duration); + if (flag()) await Future.delayed(duration); return true; }, ); diff --git a/pubspec.lock b/pubspec.lock index bc44f25..4eed3b5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -189,7 +189,7 @@ packages: name: cupertino_icons url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.4" + version: "1.0.5" dart_style: dependency: transitive description: @@ -264,7 +264,7 @@ packages: name: flutter_native_splash url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.2" + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -449,7 +449,7 @@ packages: name: path_provider url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.10" + version: "2.0.11" path_provider_android: dependency: transitive description: