diff --git a/lib/bean/appbar/sys_app_bar.dart b/lib/bean/appbar/sys_app_bar.dart index cf4affbe..fbf56eba 100644 --- a/lib/bean/appbar/sys_app_bar.dart +++ b/lib/bean/appbar/sys_app_bar.dart @@ -1,10 +1,11 @@ import 'dart:io'; -import 'package:kazumi/bean/dialog/dialog_helper.dart'; -import 'package:kazumi/utils/utils.dart'; + import 'package:flutter/material.dart'; -import 'package:window_manager/window_manager.dart'; import 'package:flutter/services.dart'; +import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/utils/storage.dart'; +import 'package:kazumi/utils/utils.dart'; +import 'package:window_manager/window_manager.dart'; class SysAppBar extends StatelessWidget implements PreferredSizeWidget { final double? toolbarHeight; @@ -41,22 +42,70 @@ class SysAppBar extends StatelessWidget implements PreferredSizeWidget { this.needTopOffset = true}); void _handleCloseEvent() { - KazumiDialog.show(builder: (context) { - return AlertDialog( - title: const Text('退出确认'), - content: const Text('您想要退出 Kazumi 吗?'), - actions: [ - TextButton(onPressed: () => exit(0), child: const Text('退出 Kazumi')), - TextButton( - onPressed: () { - KazumiDialog.dismiss(); - windowManager.hide(); - }, - child: const Text('最小化至托盘')), - const TextButton(onPressed: KazumiDialog.dismiss, child: Text('取消')), - ], - ); - }); + final setting = GStorage.setting; + final exitBehavior = + setting.get(SettingBoxKey.exitBehavior, defaultValue: 2); + + switch (exitBehavior) { + case 0: + exit(0); + case 1: + KazumiDialog.dismiss(); + windowManager.hide(); + break; + default: + KazumiDialog.show(builder: (context) { + bool saveExitBehavior = false; // 下次不再询问? + + return AlertDialog( + title: const Text('退出确认'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text('您想要退出 Kazumi 吗?'), + const SizedBox(height: 24), + StatefulBuilder(builder: (context, setState) { + onChanged(value) { + saveExitBehavior = value ?? false; + setState(() {}); + } + + return Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 8, + children: [ + Checkbox(value: saveExitBehavior, onChanged: onChanged), + const Text('下次不再询问'), + ], + ); + }), + ], + ), + actions: [ + TextButton( + onPressed: () async { + if (saveExitBehavior) { + await setting.put(SettingBoxKey.exitBehavior, 0); + } + exit(0); + }, + child: const Text('退出 Kazumi')), + TextButton( + onPressed: () async { + if (saveExitBehavior) { + await setting.put(SettingBoxKey.exitBehavior, 1); + } + KazumiDialog.dismiss(); + windowManager.hide(); + }, + child: const Text('最小化至托盘')), + const TextButton( + onPressed: KazumiDialog.dismiss, child: Text('取消')), + ], + ); + }); + } } bool showWindowButton() { diff --git a/lib/pages/about/about_page.dart b/lib/pages/about/about_page.dart index f55b1775..fcb9cd52 100644 --- a/lib/pages/about/about_page.dart +++ b/lib/pages/about/about_page.dart @@ -1,16 +1,18 @@ import 'dart:io'; + +import 'package:card_settings_ui/card_settings_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:hive/hive.dart'; -import 'package:kazumi/pages/my/my_controller.dart'; -import 'package:kazumi/request/api.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; -import 'package:card_settings_ui/card_settings_ui.dart'; -import 'package:kazumi/utils/storage.dart'; +import 'package:kazumi/pages/my/my_controller.dart'; +import 'package:kazumi/request/api.dart'; import 'package:kazumi/utils/mortis.dart'; +import 'package:kazumi/utils/storage.dart'; +import 'package:kazumi/utils/utils.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:url_launcher/url_launcher.dart'; class AboutPage extends StatefulWidget { const AboutPage({super.key}); @@ -20,10 +22,14 @@ class AboutPage extends StatefulWidget { } class _AboutPageState extends State { + final exitBehaviorTitles = ['退出 Kazumi', '最小化至托盘', '每次都询问']; + late dynamic defaultDanmakuArea; late dynamic defaultThemeMode; late dynamic defaultThemeColor; Box setting = GStorage.setting; + late int exitBehavior = + setting.get(SettingBoxKey.exitBehavior, defaultValue: 2); late bool autoUpdate; double _cacheSizeMB = -1; final MyController myController = Modular.get(); @@ -31,8 +37,7 @@ class _AboutPageState extends State { @override void initState() { super.initState(); - autoUpdate = - setting.get(SettingBoxKey.autoUpdate, defaultValue: true); + autoUpdate = setting.get(SettingBoxKey.autoUpdate, defaultValue: true); _getCacheSize(); } @@ -142,7 +147,9 @@ class _AboutPageState extends State { }, title: const Text('开源许可证'), description: const Text('查看所有开源许可证'), - ),],), + ), + ], + ), SettingsSection( title: const Text('外部链接'), tiles: [ @@ -178,7 +185,42 @@ class _AboutPageState extends State { title: const Text('弹幕来源'), description: Text('ID: ${mortis['id']}'), value: const Text('DanDanPlay'), - ),],), + ), + ], + ), + if (Utils.isDesktop()) // 之后如果有非桌面平台的新选项可以移除 + SettingsSection( + title: const Text('默认行为'), + tiles: [ + // if (Utils.isDesktop()) + SettingsTile.navigation( + title: const Text('关闭时'), + value: Text(exitBehaviorTitles[exitBehavior]), + onPressed: (_) { + KazumiDialog.show(builder: (context) { + return SimpleDialog( + title: const Text('关闭时'), + children: [ + for (int i = 0; i < 3; i++) + RadioListTile( + value: i, + groupValue: exitBehavior, + onChanged: (int? value) { + exitBehavior = value ?? 2; + setting.put( + SettingBoxKey.exitBehavior, value); + KazumiDialog.dismiss(); + setState(() {}); + }, + title: Text(exitBehaviorTitles[i]), + ), + ], + ); + }); + }, + ), + ], + ), SettingsSection( tiles: [ SettingsTile.navigation( @@ -186,7 +228,9 @@ class _AboutPageState extends State { Modular.to.pushNamed('/settings/about/logs'); }, title: const Text('错误日志'), - ),],), + ), + ], + ), SettingsSection( tiles: [ SettingsTile.navigation( @@ -197,7 +241,9 @@ class _AboutPageState extends State { value: _cacheSizeMB == -1 ? const Text('统计中...') : Text('${_cacheSizeMB.toStringAsFixed(2)}MB'), - ),],), + ), + ], + ), SettingsSection( title: const Text('应用更新'), tiles: [ diff --git a/lib/pages/my/my_page.dart b/lib/pages/my/my_page.dart index 948322dd..41d4b1c3 100644 --- a/lib/pages/my/my_page.dart +++ b/lib/pages/my/my_page.dart @@ -2,10 +2,10 @@ import 'package:card_settings_ui/card_settings_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; -import 'package:provider/provider.dart'; import 'package:kazumi/pages/menu/menu.dart'; import 'package:kazumi/pages/menu/side_menu.dart'; import 'package:kazumi/utils/utils.dart'; +import 'package:provider/provider.dart'; class MyPage extends StatefulWidget { const MyPage({super.key}); diff --git a/lib/pages/player/episode_comments_sheet.dart b/lib/pages/player/episode_comments_sheet.dart index f82a3f7e..226f718d 100644 --- a/lib/pages/player/episode_comments_sheet.dart +++ b/lib/pages/player/episode_comments_sheet.dart @@ -15,22 +15,12 @@ class EpisodeCommentsSheet extends StatefulWidget { State createState() => _EpisodeCommentsSheetState(); } -class _EpisodeCommentsSheetState extends State { +class _EpisodeCommentsSheetState extends State + with AutomaticKeepAliveClientMixin { final infoController = Modular.get(); bool isLoading = false; bool commentsQueryTimeout = false; - @override - void initState() { - super.initState(); - if (infoController.episodeCommentsList.isEmpty) { - setState(() { - isLoading = true; - }); - loadComments(widget.episode); - } - } - Future loadComments(int episode) async { commentsQueryTimeout = false; infoController @@ -56,39 +46,60 @@ class _EpisodeCommentsSheetState extends State { } Widget get episodeCommentsBody { - return SelectionArea( - child: CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.fromLTRB(4, 0, 4, 4), - sliver: Observer(builder: (context) { - if (isLoading) { - return const SliverFillRemaining( - child: Center( - child: CircularProgressIndicator(), - ), - ); - } - if (commentsQueryTimeout) { - return const SliverFillRemaining( - child: Center( - child: Text('空空如也'), - ), - ); - } - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return EpisodeCommentsCard( - commentItem: infoController.episodeCommentsList[index]); - }, - childCount: infoController.episodeCommentsList.length, + if (infoController.episodeCommentsList.isEmpty) { + setState(() { + isLoading = true; + }); + loadComments(widget.episode); + } + return CustomScrollView( + // Scrollbars' movement is not linear so hide it. + scrollBehavior: const ScrollBehavior().copyWith(scrollbars: false), + slivers: [ + SliverPadding( + padding: const EdgeInsets.fromLTRB(4, 0, 4, 4), + sliver: Observer(builder: (context) { + if (isLoading) { + return const SliverFillRemaining( + child: Center( + child: CircularProgressIndicator(), ), ); - }), - ), - ], - ), + } + if (commentsQueryTimeout) { + return const SliverFillRemaining( + child: Center( + child: Text('空空如也'), + ), + ); + } + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + // Fix scroll issue caused by height change of network images + // by keeping loaded cards alive. + return KeepAlive( + keepAlive: true, + child: IndexedSemantics( + index: index, + child: SelectionArea( + child: EpisodeCommentsCard( + commentItem: + infoController.episodeCommentsList[index], + ), + ), + ), + ); + }, + childCount: infoController.episodeCommentsList.length, + addAutomaticKeepAlives: false, + addRepaintBoundaries: false, + addSemanticIndexes: false, + ), + ); + }), + ), + ], ); } @@ -192,6 +203,7 @@ class _EpisodeCommentsSheetState extends State { @override Widget build(BuildContext context) { + super.build(context); return Scaffold( body: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -199,4 +211,7 @@ class _EpisodeCommentsSheetState extends State { ), ); } + + @override + bool get wantKeepAlive => true; } diff --git a/lib/pages/settings/danmaku/danmaku_settings_window.dart b/lib/pages/settings/danmaku/danmaku_settings_window.dart index 011318ab..1ae5e355 100644 --- a/lib/pages/settings/danmaku/danmaku_settings_window.dart +++ b/lib/pages/settings/danmaku/danmaku_settings_window.dart @@ -29,7 +29,7 @@ class _DanmakuSettingsWindowState extends State { value: widget.danmakuController.option.fontSize, min: 10, max: Utils.isCompact() ? 32 : 48, - divisions: 22, + divisions: Utils.isCompact() ? 22 : 38, label: widget.danmakuController.option.fontSize.toString(), onChanged: (value) { setState(() => widget.danmakuController.updateOption( diff --git a/lib/pages/video/video_page.dart b/lib/pages/video/video_page.dart index 5aad29e2..963d7f30 100644 --- a/lib/pages/video/video_page.dart +++ b/lib/pages/video/video_page.dart @@ -147,6 +147,7 @@ class _VideoPageState extends State _videoURLSubscription.cancel(); _logSubscription.cancel(); playerController.dispose(); + infoController.episodeCommentsList.clear(); Utils.unlockScreenRotation(); super.dispose(); } @@ -405,10 +406,8 @@ class _VideoPageState extends State position: _rightOffsetAnimation, child: SizedBox( height: MediaQuery.of(context).size.height, - width: videoPageController.isFullscreen - ? (Utils.isTablet() - ? MediaQuery.of(context).size.width / 3 - : MediaQuery.of(context).size.height) + width: !isWideScreen + ? MediaQuery.of(context).size.height : (MediaQuery.of(context).size.width / 3 > 420 ? 420 : MediaQuery.of(context).size.width / 3), diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 7db4c2d7..c17ac524 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -1,11 +1,12 @@ import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/modules/bangumi/bangumi_tag.dart'; -import 'package:kazumi/modules/history/history_module.dart'; import 'package:kazumi/modules/collect/collect_module.dart'; +import 'package:kazumi/modules/history/history_module.dart'; +import 'package:path_provider/path_provider.dart'; class GStorage { // Don't use favorites box, it's replaced by collectibles. @@ -81,7 +82,8 @@ class GStorage { static Future patchCollectibles(String backupFilePath) async { final backupFile = File(backupFilePath); final backupContent = await backupFile.readAsBytes(); - final tempBox = await Hive.openBox('tempCollectiblesBox', bytes: backupContent); + final tempBox = + await Hive.openBox('tempCollectiblesBox', bytes: backupContent); final tempBoxItems = tempBox.toMap().entries; debugPrint('webDav追番列表长度 ${tempBoxItems.length}'); @@ -136,5 +138,6 @@ class SettingBoxKey { lowMemoryMode = 'lowMemoryMode', useDynamicColor = 'useDynamicColor', defaultSuperResolutionType = 'defaultSuperResolutionType', + exitBehavior = 'exitBehavior', showWindowButton = 'showWindowButton'; }