From 6dd2014f1bc65ed4ee275352b3a33c3bde538ecc Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Sun, 5 Jul 2026 13:58:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=A1=B9=E7=9B=AE=E7=BB=93?= =?UTF-8?q?=E6=9E=842?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...kotlin-compiler-1550729391791673956.salive | 0 lib/main.dart | 8 +- lib/pages/book/book_detail_page.dart | 2 +- lib/pages/book/book_excerpts_page.dart | 2 +- lib/pages/epub_reader/book_link_page.dart | 4 +- lib/pages/epub_reader/book_session.dart | 6 +- lib/pages/epub_reader/epub_detail_page.dart | 10 +- lib/pages/epub_reader/epub_edit_page.dart | 2 +- .../epub_reader/epub_highlights_page.dart | 2 +- lib/pages/epub_reader/epub_library_page.dart | 4 +- lib/pages/epub_reader/reader_renderer.dart | 6 +- lib/pages/epub_reader/reader_screen.dart | 16 +- lib/pages/epub_reader/reader_style_sheet.dart | 2 +- lib/pages/epub_reader/reader_webview.dart | 10 +- lib/pages/epub_reader/search_sheet.dart | 2 +- lib/pages/epub_reader/toc_drawer.dart | 2 +- lib/pages/explore/encounter_page.dart | 4 +- lib/pages/explore/media_calendar_page.dart | 12 +- lib/pages/explore/person_list_page.dart | 8 +- lib/pages/explore/statistics_page.dart | 8 +- lib/pages/explore/stroll_page.dart | 14 +- lib/pages/home/home_page.dart | 14 +- lib/pages/home/main_content_page.dart | 18 +- .../enhanced_search_settings_page.dart | 2 +- lib/pages/profile/app_icon_picker_page.dart | 2 +- lib/pages/profile/changelog_page.dart | 2 +- lib/pages/profile/feature_settings_page.dart | 415 ++++ lib/pages/profile/font_picker_page.dart | 6 +- lib/pages/profile/layout_settings_page.dart | 276 +++ lib/pages/profile/profile_page.dart | 2117 +---------------- lib/pages/profile/settings_page.dart | 1414 +++++++++++ lib/pages/settings/legal_page.dart | 2 +- lib/pages/settings/recycle_bin_page.dart | 6 +- lib/pages/settings/tag_management_page.dart | 4 +- lib/pages/sync/backup_page.dart | 4 +- lib/pages/sync/webdav_sync_page.dart | 2 +- lib/providers/app_provider.dart | 20 +- lib/services/changelog_service.dart | 2 +- lib/services/epub/epub_parser.dart | 2 +- lib/services/epub/epub_service.dart | 4 +- lib/services/sync/backup_service.dart | 4 +- lib/services/usage_stats_service.dart | 6 +- lib/widgets/custom_drawer.dart | 10 +- 43 files changed, 2235 insertions(+), 2221 deletions(-) delete mode 100644 android/.kotlin/sessions/kotlin-compiler-1550729391791673956.salive create mode 100644 lib/pages/profile/feature_settings_page.dart create mode 100644 lib/pages/profile/layout_settings_page.dart create mode 100644 lib/pages/profile/settings_page.dart diff --git a/android/.kotlin/sessions/kotlin-compiler-1550729391791673956.salive b/android/.kotlin/sessions/kotlin-compiler-1550729391791673956.salive deleted file mode 100644 index e69de29..0000000 diff --git a/lib/main.dart b/lib/main.dart index bc3f243..b426cba 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,13 +9,13 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import 'pages/home_page.dart'; +import 'pages/home/home_page.dart'; import 'utils/theme/app_theme.dart'; import 'utils/app_router.dart'; import 'utils/user_prefs.dart'; -import 'utils/changelog_service.dart'; -import 'utils/sync/auto_backup_service.dart'; -import 'utils/usage_stats_service.dart'; +import 'services/changelog_service.dart'; +import 'services/sync/auto_backup_service.dart'; +import 'services/usage_stats_service.dart'; import 'providers/app_provider.dart'; final RouteObserver> routeObserver = RouteObserver>(); diff --git a/lib/pages/book/book_detail_page.dart b/lib/pages/book/book_detail_page.dart index 00638ea..0b3a804 100644 --- a/lib/pages/book/book_detail_page.dart +++ b/lib/pages/book/book_detail_page.dart @@ -11,7 +11,7 @@ import '../../utils/user_prefs.dart'; import 'book_reviews_page.dart'; import 'book_excerpts_page.dart'; import 'book_share_page.dart'; -import '../../utils/epub/reader_dao.dart'; +import '../../data/epub/reader_dao.dart'; import '../epub_reader/epub_highlights_page.dart'; import '../epub_reader/reader_screen.dart'; diff --git a/lib/pages/book/book_excerpts_page.dart b/lib/pages/book/book_excerpts_page.dart index 25968ef..361c915 100644 --- a/lib/pages/book/book_excerpts_page.dart +++ b/lib/pages/book/book_excerpts_page.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import '../../providers/app_provider.dart'; import '../../models/data_models.dart'; import '../../utils/toast_util.dart'; -import '../../utils/epub/reader_dao.dart'; +import '../../data/epub/reader_dao.dart'; import '../../widgets/fade_in_local_image.dart'; import 'book_excerpt_form_page.dart'; diff --git a/lib/pages/epub_reader/book_link_page.dart b/lib/pages/epub_reader/book_link_page.dart index f3fe03a..633b6df 100644 --- a/lib/pages/epub_reader/book_link_page.dart +++ b/lib/pages/epub_reader/book_link_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../utils/book/book_dao.dart'; -import '../../utils/epub/reader_dao.dart'; +import '../../data/book/book_dao.dart'; +import '../../data/epub/reader_dao.dart'; import '../../utils/toast_util.dart'; import '../../models/data_models.dart'; import '../../widgets/fade_in_local_image.dart'; diff --git a/lib/pages/epub_reader/book_session.dart b/lib/pages/epub_reader/book_session.dart index 3daa832..0cb9196 100644 --- a/lib/pages/epub_reader/book_session.dart +++ b/lib/pages/epub_reader/book_session.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import '../../utils/epub/epub_webview_handler.dart'; -import '../../utils/epub/reader_dao.dart'; -import '../../utils/epub/reader_models.dart'; +import '../../services/epub/epub_webview_handler.dart'; +import '../../data/epub/reader_dao.dart'; +import '../../data/epub/reader_models.dart'; /// Manages the current reading session including book data, TOC state, and /// progress tracking. Adapted from lumina's BookSession but uses mooknote's diff --git a/lib/pages/epub_reader/epub_detail_page.dart b/lib/pages/epub_reader/epub_detail_page.dart index 9ee13e9..8e4e9c0 100644 --- a/lib/pages/epub_reader/epub_detail_page.dart +++ b/lib/pages/epub_reader/epub_detail_page.dart @@ -3,11 +3,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import '../../utils/epub/reader_dao.dart'; -import '../../utils/epub/epub_parser.dart'; -import '../../utils/epub/reader_models.dart'; -import '../../utils/book/book_dao.dart'; -import '../../utils/book/book_excerpt_dao.dart'; +import '../../data/epub/reader_dao.dart'; +import '../../services/epub/epub_parser.dart'; +import '../../data/epub/reader_models.dart'; +import '../../data/book/book_dao.dart'; +import '../../data/book/book_excerpt_dao.dart'; import '../../utils/toast_util.dart'; import '../../models/data_models.dart'; import '../book/book_detail_page.dart'; diff --git a/lib/pages/epub_reader/epub_edit_page.dart b/lib/pages/epub_reader/epub_edit_page.dart index ad7708d..0fc686d 100644 --- a/lib/pages/epub_reader/epub_edit_page.dart +++ b/lib/pages/epub_reader/epub_edit_page.dart @@ -9,7 +9,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import '../../providers/app_provider.dart'; -import '../../utils/epub/reader_dao.dart'; +import '../../data/epub/reader_dao.dart'; import '../../widgets/genre_selector_page.dart'; import '../../widgets/text_input_panel.dart'; diff --git a/lib/pages/epub_reader/epub_highlights_page.dart b/lib/pages/epub_reader/epub_highlights_page.dart index 136da9a..d738e79 100644 --- a/lib/pages/epub_reader/epub_highlights_page.dart +++ b/lib/pages/epub_reader/epub_highlights_page.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import '../../utils/epub/reader_dao.dart'; +import '../../data/epub/reader_dao.dart'; import '../../utils/toast_util.dart'; import '../../utils/user_prefs.dart'; import 'highlight_detail_sheet.dart'; diff --git a/lib/pages/epub_reader/epub_library_page.dart b/lib/pages/epub_reader/epub_library_page.dart index ff15c8d..b4cedbe 100644 --- a/lib/pages/epub_reader/epub_library_page.dart +++ b/lib/pages/epub_reader/epub_library_page.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; -import '../../utils/epub/reader_dao.dart'; -import '../../utils/epub/epub_service.dart'; +import '../../data/epub/reader_dao.dart'; +import '../../services/epub/epub_service.dart'; import '../../utils/user_prefs.dart'; import '../../utils/responsive.dart'; import 'epub_detail_page.dart'; diff --git a/lib/pages/epub_reader/reader_renderer.dart b/lib/pages/epub_reader/reader_renderer.dart index 6fc656f..3e3711f 100644 --- a/lib/pages/epub_reader/reader_renderer.dart +++ b/lib/pages/epub_reader/reader_renderer.dart @@ -4,9 +4,9 @@ import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; -import '../../utils/epub/epub_theme.dart'; -import '../../utils/epub/reader_settings.dart'; -import '../../utils/epub/epub_webview_handler.dart'; +import '../../services/epub/epub_theme.dart'; +import '../../services/epub/reader_settings.dart'; +import '../../services/epub/epub_webview_handler.dart'; import 'book_session.dart'; import 'reader_webview.dart'; import 'page_turn/page_turn.dart'; diff --git a/lib/pages/epub_reader/reader_screen.dart b/lib/pages/epub_reader/reader_screen.dart index b171c4f..4755b75 100644 --- a/lib/pages/epub_reader/reader_screen.dart +++ b/lib/pages/epub_reader/reader_screen.dart @@ -4,14 +4,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../utils/database_helper.dart'; -import '../../utils/epub/epub_theme.dart'; -import '../../utils/epub/epub_webview_handler.dart'; -import '../../utils/epub/epub_stream_service.dart'; -import '../../utils/epub/epub_parser.dart'; -import '../../utils/epub/reader_settings.dart'; -import '../../utils/epub/reader_models.dart'; -import '../../utils/epub/reader_dao.dart'; +import '../../data/database_helper.dart'; +import '../../services/epub/epub_theme.dart'; +import '../../services/epub/epub_webview_handler.dart'; +import '../../services/epub/epub_stream_service.dart'; +import '../../services/epub/epub_parser.dart'; +import '../../services/epub/reader_settings.dart'; +import '../../data/epub/reader_models.dart'; +import '../../data/epub/reader_dao.dart'; import '../../utils/toast_util.dart'; import 'book_session.dart'; import 'reader_renderer.dart'; diff --git a/lib/pages/epub_reader/reader_style_sheet.dart b/lib/pages/epub_reader/reader_style_sheet.dart index 2857533..141c997 100644 --- a/lib/pages/epub_reader/reader_style_sheet.dart +++ b/lib/pages/epub_reader/reader_style_sheet.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import '../../utils/epub/epub_theme.dart'; +import '../../services/epub/epub_theme.dart'; import 'widgets/integer_stepper.dart'; import 'widgets/reader_scale_slider.dart'; diff --git a/lib/pages/epub_reader/reader_webview.dart b/lib/pages/epub_reader/reader_webview.dart index cbc9868..547e1a2 100644 --- a/lib/pages/epub_reader/reader_webview.dart +++ b/lib/pages/epub_reader/reader_webview.dart @@ -5,12 +5,12 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import '../../utils/epub/epub_theme.dart'; +import '../../services/epub/epub_theme.dart'; import 'book_session.dart'; -import '../../utils/epub/epub_webview_handler.dart'; -import '../../utils/epub/reader_scripts.dart'; -import '../../utils/epub/web/webview_bridge.dart'; -import '../../utils/epub/web/reader_api.dart'; +import '../../services/epub/epub_webview_handler.dart'; +import '../../services/epub/reader_scripts.dart'; +import '../../services/epub/web/webview_bridge.dart'; +import '../../services/epub/web/reader_api.dart'; /// Controller for ReaderWebView that provides methods to control the WebView class ReaderWebViewController { diff --git a/lib/pages/epub_reader/search_sheet.dart b/lib/pages/epub_reader/search_sheet.dart index 99fad0d..6e6d5de 100644 --- a/lib/pages/epub_reader/search_sheet.dart +++ b/lib/pages/epub_reader/search_sheet.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:xml/xml.dart'; -import '../../utils/epub/epub_stream_service.dart'; +import '../../services/epub/epub_stream_service.dart'; import 'book_session.dart'; class SearchSheet extends StatefulWidget { diff --git a/lib/pages/epub_reader/toc_drawer.dart b/lib/pages/epub_reader/toc_drawer.dart index 9a8e848..544938e 100644 --- a/lib/pages/epub_reader/toc_drawer.dart +++ b/lib/pages/epub_reader/toc_drawer.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import '../../utils/epub/reader_models.dart'; +import '../../data/epub/reader_models.dart'; /// Helper class to represent a visible row in the flattened TOC list class _TocRowItem { diff --git a/lib/pages/explore/encounter_page.dart b/lib/pages/explore/encounter_page.dart index 14c9776..5bd18dd 100644 --- a/lib/pages/explore/encounter_page.dart +++ b/lib/pages/explore/encounter_page.dart @@ -1,8 +1,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../providers/app_provider.dart'; -import '../utils/user_prefs.dart'; +import '../../providers/app_provider.dart'; +import '../../utils/user_prefs.dart'; /// 相遇统计页 class EncounterPage extends StatelessWidget { diff --git a/lib/pages/explore/media_calendar_page.dart b/lib/pages/explore/media_calendar_page.dart index 63c3a7e..578d2c2 100644 --- a/lib/pages/explore/media_calendar_page.dart +++ b/lib/pages/explore/media_calendar_page.dart @@ -1,12 +1,12 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../providers/app_provider.dart'; -import '../models/data_models.dart'; -import 'movies/movie_detail_page.dart'; -import 'movies/movie_form_page.dart'; -import 'book/book_detail_page.dart'; -import 'book/book_form_page.dart'; +import '../../providers/app_provider.dart'; +import '../../models/data_models.dart'; +import '../movies/movie_detail_page.dart'; +import '../movies/movie_form_page.dart'; +import '../book/book_detail_page.dart'; +import '../book/book_form_page.dart'; /// 书影日历 - 按月展示影视/书籍添加记录 class MediaCalendarPage extends StatefulWidget { diff --git a/lib/pages/explore/person_list_page.dart b/lib/pages/explore/person_list_page.dart index 5341b3f..0d575e8 100644 --- a/lib/pages/explore/person_list_page.dart +++ b/lib/pages/explore/person_list_page.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../providers/app_provider.dart'; -import '../models/data_models.dart'; -import 'movies/movie_detail_page.dart'; -import 'book/book_detail_page.dart'; +import '../../providers/app_provider.dart'; +import '../../models/data_models.dart'; +import '../movies/movie_detail_page.dart'; +import '../book/book_detail_page.dart'; /// 角色信息页面 - 列出所有导演/主演/编剧/作者 class PersonListPage extends StatefulWidget { diff --git a/lib/pages/explore/statistics_page.dart b/lib/pages/explore/statistics_page.dart index fa804ca..26b2865 100644 --- a/lib/pages/explore/statistics_page.dart +++ b/lib/pages/explore/statistics_page.dart @@ -2,10 +2,10 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:provider/provider.dart'; -import '../providers/app_provider.dart'; -import '../models/data_models.dart'; -import '../utils/user_prefs.dart'; -import '../widgets/fade_in_local_image.dart'; +import '../../providers/app_provider.dart'; +import '../../models/data_models.dart'; +import '../../utils/user_prefs.dart'; +import '../../widgets/fade_in_local_image.dart'; /// 数据统计页面 - 多维度数据分析 class StatisticsPage extends StatefulWidget { diff --git a/lib/pages/explore/stroll_page.dart b/lib/pages/explore/stroll_page.dart index abe42c0..14b5f25 100644 --- a/lib/pages/explore/stroll_page.dart +++ b/lib/pages/explore/stroll_page.dart @@ -1,13 +1,13 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../providers/app_provider.dart'; -import '../models/data_models.dart'; -import '../widgets/fade_in_local_image.dart'; -import '../utils/toast_util.dart'; -import 'movies/movie_detail_page.dart'; -import 'book/book_detail_page.dart'; -import 'note/note_detail_page.dart'; +import '../../providers/app_provider.dart'; +import '../../models/data_models.dart'; +import '../../widgets/fade_in_local_image.dart'; +import '../../utils/toast_util.dart'; +import '../movies/movie_detail_page.dart'; +import '../book/book_detail_page.dart'; +import '../note/note_detail_page.dart'; /// 漫步页面 - 随机发现内容 class StrollPage extends StatefulWidget { diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 84d83fc..1a4e3b9 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../providers/app_provider.dart'; -import '../utils/user_prefs.dart'; -import '../utils/responsive.dart'; -import '../widgets/custom_drawer.dart'; -import '../widgets/bottom_nav_bar.dart'; -import '../widgets/add_sheet.dart'; +import '../../providers/app_provider.dart'; +import '../../utils/user_prefs.dart'; +import '../../utils/responsive.dart'; +import '../../widgets/custom_drawer.dart'; +import '../../widgets/bottom_nav_bar.dart'; +import '../../widgets/add_sheet.dart'; import 'main_content_page.dart'; -import 'profile_page.dart'; +import '../profile/profile_page.dart'; /// 主页 - 包含底部导航,可切换主页/我的 class HomePage extends StatefulWidget { diff --git a/lib/pages/home/main_content_page.dart b/lib/pages/home/main_content_page.dart index 784dac8..e106acf 100644 --- a/lib/pages/home/main_content_page.dart +++ b/lib/pages/home/main_content_page.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../providers/app_provider.dart'; -import '../utils/user_prefs.dart'; -import '../utils/sync/webdav_service.dart'; -import 'movies/movie_tab_page.dart'; -import 'book/book_tab_page.dart'; -import 'note/note_tab_page.dart'; -import 'online_search/search_page.dart'; -import 'online_search/online_search_page.dart'; -import 'sync/webdav_sync_page.dart'; +import '../../providers/app_provider.dart'; +import '../../utils/user_prefs.dart'; +import '../../services/sync/webdav_service.dart'; +import '../movies/movie_tab_page.dart'; +import '../book/book_tab_page.dart'; +import '../note/note_tab_page.dart'; +import '../online_search/search_page.dart'; +import '../online_search/online_search_page.dart'; +import '../sync/webdav_sync_page.dart'; /// 主内容页 - 观影/阅读/笔记标签页(PageView 滑动切换) class MainContentPage extends StatefulWidget { diff --git a/lib/pages/online_search/enhanced_search_settings_page.dart b/lib/pages/online_search/enhanced_search_settings_page.dart index 111c1fa..6915b4e 100644 --- a/lib/pages/online_search/enhanced_search_settings_page.dart +++ b/lib/pages/online_search/enhanced_search_settings_page.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import '../../utils/user_prefs.dart'; import '../../utils/server_config.dart'; -import '../legal_page.dart'; +import '../settings/legal_page.dart'; /// 增强搜索设置页面 class EnhancedSearchSettingsPage extends StatefulWidget { diff --git a/lib/pages/profile/app_icon_picker_page.dart b/lib/pages/profile/app_icon_picker_page.dart index 24bd941..90c335a 100644 --- a/lib/pages/profile/app_icon_picker_page.dart +++ b/lib/pages/profile/app_icon_picker_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../../utils/user_prefs.dart'; import '../../utils/toast_util.dart'; -import '../../utils/app_icon_channel.dart'; +import '../../services/app_icon_channel.dart'; /// 应用图标选择页面 class AppIconPickerPage extends StatefulWidget { diff --git a/lib/pages/profile/changelog_page.dart b/lib/pages/profile/changelog_page.dart index 13d5ba7..0c9602e 100644 --- a/lib/pages/profile/changelog_page.dart +++ b/lib/pages/profile/changelog_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../utils/changelog_service.dart'; +import '../../services/changelog_service.dart'; /// 更新日志页面 class ChangelogPage extends StatefulWidget { diff --git a/lib/pages/profile/feature_settings_page.dart b/lib/pages/profile/feature_settings_page.dart new file mode 100644 index 0000000..2c7bf44 --- /dev/null +++ b/lib/pages/profile/feature_settings_page.dart @@ -0,0 +1,415 @@ +import 'package:flutter/material.dart'; +import '../../utils/user_prefs.dart'; +import '../../utils/toast_util.dart'; + +class FeatureSettingsPage extends StatefulWidget { + const FeatureSettingsPage({super.key}); + + @override + State createState() => _FeatureSettingsPageState(); +} + +class _FeatureSettingsPageState extends State { + final UserPrefs _userPrefs = UserPrefs(); + + // 主界面 + bool _showMovieTab = true; + bool _showBookTab = true; + bool _showNoteTab = true; + int _defaultTabIndex = 0; + + // 侧边栏 + bool _showHeatmap = true; + bool _showRecent = true; + bool _showEncounter = true; + bool _showStroll = true; + bool _showCalendar = true; + bool _showPerson = true; + bool _showTags = true; + bool _showMdReader = true; + bool _showEpub = true; + + @override + void initState() { + super.initState(); + _loadSettings(); + } + + void _loadSettings() { + setState(() { + _showMovieTab = _userPrefs.showMovieTab; + _showBookTab = _userPrefs.showBookTab; + _showNoteTab = _userPrefs.showNoteTab; + _defaultTabIndex = _userPrefs.defaultMainTabIndex; + _showHeatmap = _userPrefs.showSidebarHeatmap; + _showRecent = _userPrefs.showSidebarRecent; + _showEncounter = _userPrefs.showSidebarEncounter; + _showStroll = _userPrefs.showSidebarStroll; + _showCalendar = _userPrefs.showSidebarCalendar; + _showPerson = _userPrefs.showSidebarPerson; + _showTags = _userPrefs.showSidebarTags; + _showMdReader = _userPrefs.showSidebarMdReader; + _showEpub = _userPrefs.showSidebarEpub; + }); + } + + int get _enabledTabCount { + int count = 0; + if (_showMovieTab) count++; + if (_showBookTab) count++; + if (_showNoteTab) count++; + return count; + } + + List<(int, String, IconData)> get _enabledTabs { + final all = [ + (0, '影视', Icons.movie_outlined), + (1, '阅读', Icons.menu_book_outlined), + (2, '笔记', Icons.note_outlined), + ]; + return all.where((t) { + return switch (t.$1) { + 0 => _showMovieTab, + 1 => _showBookTab, + 2 => _showNoteTab, + _ => false, + }; + }).toList(); + } + + void _fixDefaultTabIndex() { + final enabled = _enabledTabs; + if (!enabled.any((t) => t.$1 == _defaultTabIndex) && enabled.isNotEmpty) { + _defaultTabIndex = enabled.first.$1; + _userPrefs.setDefaultMainTabIndex(_defaultTabIndex); + } + } + + Future _toggleMovieTab(bool value) async { + if (!value && _enabledTabCount <= 1) { + ToastUtil.show(context, '至少保留一个标签页'); + return; + } + await _userPrefs.setShowMovieTab(value); + setState(() { + _showMovieTab = value; + _fixDefaultTabIndex(); + }); + } + + Future _toggleBookTab(bool value) async { + if (!value && _enabledTabCount <= 1) { + ToastUtil.show(context, '至少保留一个标签页'); + return; + } + await _userPrefs.setShowBookTab(value); + setState(() { + _showBookTab = value; + _fixDefaultTabIndex(); + }); + } + + Future _toggleNoteTab(bool value) async { + if (!value && _enabledTabCount <= 1) { + ToastUtil.show(context, '至少保留一个标签页'); + return; + } + await _userPrefs.setShowNoteTab(value); + setState(() { + _showNoteTab = value; + _fixDefaultTabIndex(); + }); + } + + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; + return Scaffold( + backgroundColor: colors.surface, + appBar: AppBar(title: const Text('功能设置')), + body: ListView( + children: [ + // ── 启动设置 ── + _buildSectionHeader('启动设置'), + _buildDefaultTabSelector(), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + + // ── 模块开关 ── + _buildSectionHeader('模块开关'), + _buildSwitchItem(Icons.movie_outlined, '观影', '记录和管理观影记录', + _showMovieTab, _toggleMovieTab), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem(Icons.menu_book_outlined, '阅读', '记录和管理阅读记录', + _showBookTab, _toggleBookTab), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem(Icons.note_outlined, '笔记', '记录和管理笔记', _showNoteTab, + _toggleNoteTab), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + // ── 侧边栏:信息模块 ── + _buildSectionHeader('侧边栏 · 信息模块'), + _buildSwitchItem( + Icons.calendar_today, '热力图', '显示创作活跃度热力图', _showHeatmap, + (v) async { + await _userPrefs.setShowSidebarHeatmap(v); + setState(() => _showHeatmap = v); + }), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem(Icons.schedule, '最近添加', '显示最近添加的记录', _showRecent, + (v) async { + await _userPrefs.setShowSidebarRecent(v); + setState(() => _showRecent = v); + }), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem( + Icons.favorite_border, '统计', '与应用相遇的天数和数据概览', _showEncounter, + (v) async { + await _userPrefs.setShowSidebarEncounter(v); + setState(() => _showEncounter = v); + }), + + // ── 侧边栏:快捷功能 ── + _buildSectionHeader('侧边栏 · 快捷功能'), + _buildSwitchItem(Icons.explore_outlined, '漫步', '随机发现内容', _showStroll, + (v) async { + await _userPrefs.setShowSidebarStroll(v); + setState(() => _showStroll = v); + }), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem( + Icons.calendar_month_outlined, '书影日历', '按日历查看记录', _showCalendar, + (v) async { + await _userPrefs.setShowSidebarCalendar(v); + setState(() => _showCalendar = v); + }), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem( + Icons.people_outline, '角色信息', '管理影视和书籍中的角色', _showPerson, + (v) async { + await _userPrefs.setShowSidebarPerson(v); + setState(() => _showPerson = v); + }), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem(Icons.label_outline, '标签管理', '管理所有标签', _showTags, + (v) async { + await _userPrefs.setShowSidebarTags(v); + setState(() => _showTags = v); + }), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem(Icons.description_outlined, 'MD阅读', 'Markdown 文件阅读器', + _showMdReader, (v) async { + await _userPrefs.setShowSidebarMdReader(v); + setState(() => _showMdReader = v); + }), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem( + Icons.auto_stories_outlined, 'EPUB阅读', 'EPUB 电子书阅读器', _showEpub, + (v) async { + await _userPrefs.setShowSidebarEpub(v); + setState(() => _showEpub = v); + }), + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Text('关闭后对应功能将从界面中隐藏。', + style: TextStyle( + fontSize: 11, + color: colors.onSurface.withValues(alpha: 0.3))), + ), + ], + ), + ); + } + + Widget _buildSectionHeader(String title) { + final colors = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.fromLTRB(24, 20, 24, 8), + child: Text(title, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: colors.onSurface)), + ); + } + + Widget _buildSwitchItem(IconData icon, String title, String subtitle, + bool value, ValueChanged onChanged) { + final colors = Theme.of(context).colorScheme; + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), + leading: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10)), + child: Icon(icon, + color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + title: Text(title, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: colors.onSurface)), + subtitle: Text(subtitle, + style: TextStyle( + fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), + trailing: Switch( + value: value, + onChanged: onChanged, + activeColor: colors.primary, + activeTrackColor: colors.primary.withValues(alpha: 0.3), + inactiveThumbColor: colors.surface, + inactiveTrackColor: colors.outline), + ); + } + + Widget _buildDefaultTabSelector() { + final colors = Theme.of(context).colorScheme; + final enabled = _enabledTabs; + final currentLabel = enabled + .firstWhere((t) => t.$1 == _defaultTabIndex, + orElse: () => enabled.first) + .$2; + + return InkWell( + onTap: _showDefaultTabPicker, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10)), + child: Icon(Icons.home_outlined, + color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + const SizedBox(width: 12), + Expanded( + child: Text('默认启动标签', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: colors.onSurface)), + ), + Text(currentLabel, + style: TextStyle( + fontSize: 13, + color: colors.onSurface.withValues(alpha: 0.5))), + const SizedBox(width: 4), + Icon(Icons.chevron_right, + size: 18, color: colors.onSurface.withValues(alpha: 0.25)), + ], + ), + ), + ); + } + + void _showDefaultTabPicker() { + final colors = Theme.of(context).colorScheme; + final enabled = _enabledTabs; + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (ctx) => Container( + decoration: BoxDecoration( + color: colors.surface, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(16))), + padding: const EdgeInsets.symmetric(vertical: 12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: colors.onSurface.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(2))), + const SizedBox(height: 20), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text('默认启动标签', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colors.onSurface)))), + const SizedBox(height: 16), + for (final t in enabled) + ListTile( + leading: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10)), + child: Icon(t.$3, + color: colors.onSurface.withValues(alpha: 0.6))), + title: Text(t.$2, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: colors.onSurface)), + trailing: _defaultTabIndex == t.$1 + ? Icon(Icons.check, color: colors.onSurface, size: 20) + : null, + onTap: () async { + await _userPrefs.setDefaultMainTabIndex(t.$1); + setState(() => _defaultTabIndex = t.$1); + Navigator.pop(ctx); + }, + ), + const SizedBox(height: 8), + ], + ), + ), + ); + } +} diff --git a/lib/pages/profile/font_picker_page.dart b/lib/pages/profile/font_picker_page.dart index fa3a8de..1cfb251 100644 --- a/lib/pages/profile/font_picker_page.dart +++ b/lib/pages/profile/font_picker_page.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; import 'package:path/path.dart' as path; import 'package:permission_handler/permission_handler.dart'; -import '../utils/font_download_manager.dart'; -import '../utils/toast_util.dart'; -import '../utils/user_prefs.dart'; +import '../../services/font_download_manager.dart'; +import '../../utils/toast_util.dart'; +import '../../utils/user_prefs.dart'; /// 字体选择页面 /// diff --git a/lib/pages/profile/layout_settings_page.dart b/lib/pages/profile/layout_settings_page.dart new file mode 100644 index 0000000..3a24406 --- /dev/null +++ b/lib/pages/profile/layout_settings_page.dart @@ -0,0 +1,276 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../providers/app_provider.dart'; +import '../../utils/user_prefs.dart'; + +class LayoutSettingsPage extends StatefulWidget { + const LayoutSettingsPage({super.key}); + + @override + State createState() => _LayoutSettingsPageState(); +} + +class _LayoutSettingsPageState extends State { + final UserPrefs _userPrefs = UserPrefs(); + int _noteLayout = 0; + int _movieLayout = 0; + int _bookLayout = 0; + bool _movieWallMode = false; + bool _bookshelfMode = false; + + @override + void initState() { + super.initState(); + _noteLayout = _userPrefs.noteLayoutStyle; + _movieLayout = _userPrefs.movieLayoutStyle; + _bookLayout = _userPrefs.bookLayoutStyle; + _movieWallMode = _userPrefs.movieWallMode; + _bookshelfMode = _userPrefs.bookshelfMode; + } + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; + return Scaffold( + backgroundColor: colors.surface, + appBar: AppBar(title: const Text('布局设置')), + body: ListView( + padding: const EdgeInsets.symmetric(vertical: 16), + children: [ + // ── 影视 ── + _buildCategoryHeader(Icons.movie_outlined, '影视', colors.primary), + _buildWallSwitch( + icon: Icons.wallpaper_outlined, + title: '影视墙模式', + subtitle: '显示全部影片,不区分状态', + value: _movieWallMode, + onChanged: (v) => _setWallMode('movie', v), + ), + _buildLayoutSelector( + selected: _movieLayout, + options: [ + (0, Icons.grid_view_outlined, '海报网格'), + (1, Icons.view_list_outlined, '列表'), + (2, Icons.crop_landscape_outlined, '大图卡片'), + ], + onChanged: (v) => _setLayout('movie', v), + ), + const SizedBox(height: 8), + Divider( + height: 1, + indent: 16, + endIndent: 16, + color: colors.outlineVariant.withValues(alpha: 0.5)), + const SizedBox(height: 8), + + // ── 阅读 ── + _buildCategoryHeader(Icons.menu_book_outlined, '阅读', colors.primary), + _buildWallSwitch( + icon: Icons.auto_stories_outlined, + title: '书架模式', + subtitle: '显示全部书籍,不区分状态', + value: _bookshelfMode, + onChanged: (v) => _setWallMode('book', v), + ), + _buildLayoutSelector( + selected: _bookLayout, + options: [ + (0, Icons.grid_view_outlined, '海报网格'), + (1, Icons.view_list_outlined, '列表'), + ], + onChanged: (v) => _setLayout('book', v), + ), + const SizedBox(height: 8), + Divider( + height: 1, + indent: 16, + endIndent: 16, + color: colors.outlineVariant.withValues(alpha: 0.5)), + const SizedBox(height: 8), + + // ── 笔记 ── + _buildCategoryHeader(Icons.note_outlined, '笔记', colors.primary), + _buildLayoutSelector( + selected: _noteLayout, + options: [ + (0, Icons.view_list_outlined, '列表'), + (1, Icons.grid_view_outlined, '瀑布流'), + (2, Icons.timeline_outlined, '时间线'), + ], + onChanged: (v) => _setLayout('note', v), + ), + ], + ), + ); + } + + Widget _buildCategoryHeader(IconData icon, String title, Color color) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), + child: Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, size: 16, color: color), + ), + const SizedBox(width: 10), + Text(title, + style: TextStyle( + fontSize: 15, fontWeight: FontWeight.w700, color: color)), + ], + ), + ); + } + + Widget _buildWallSwitch({ + required IconData icon, + required String title, + required String subtitle, + required bool value, + required ValueChanged onChanged, + }) { + final colors = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: colors.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colors.primary.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, + size: 20, color: colors.primary.withValues(alpha: 0.8)), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colors.onSurface)), + const SizedBox(height: 2), + Text(subtitle, + style: TextStyle( + fontSize: 12, + color: colors.onSurface.withValues(alpha: 0.5))), + ], + ), + ), + Switch( + value: value, + onChanged: onChanged, + activeThumbColor: colors.primary, + ), + ], + ), + ), + ); + } + + void _setWallMode(String type, bool value) async { + switch (type) { + case 'movie': + await _userPrefs.setMovieWallMode(value); + setState(() => _movieWallMode = value); + if (mounted) context.read().setMovieWallMode(value); + case 'book': + await _userPrefs.setBookshelfMode(value); + setState(() => _bookshelfMode = value); + if (mounted) context.read().setBookshelfMode(value); + } + } + + void _setLayout(String type, int value) async { + switch (type) { + case 'note': + await _userPrefs.setNoteLayoutStyle(value); + setState(() => _noteLayout = value); + case 'movie': + await _userPrefs.setMovieLayoutStyle(value); + setState(() => _movieLayout = value); + if (mounted) context.read().setMovieLayoutStyle(value); + case 'book': + await _userPrefs.setBookLayoutStyle(value); + setState(() => _bookLayout = value); + } + } + + Widget _buildLayoutSelector({ + required int selected, + required List<(int, IconData, String)> options, + required ValueChanged onChanged, + }) { + final colors = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: Row( + children: options.map((opt) { + final isSelected = selected == opt.$1; + return Expanded( + child: GestureDetector( + onTap: () => onChanged(opt.$1), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: isSelected + ? colors.primary + : colors.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? colors.primary + : colors.outlineVariant.withValues(alpha: 0.5), + width: isSelected ? 0 : 0.5, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + opt.$2, + size: 22, + color: isSelected + ? colors.onPrimary + : colors.onSurface.withValues(alpha: 0.5), + ), + const SizedBox(height: 6), + Text( + opt.$3, + style: TextStyle( + fontSize: 12, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w500, + color: isSelected + ? colors.onPrimary + : colors.onSurface.withValues(alpha: 0.5), + ), + ), + ], + ), + ), + ), + ); + }).toList(), + ), + ); + } +} diff --git a/lib/pages/profile/profile_page.dart b/lib/pages/profile/profile_page.dart index 6235662..7138b5a 100644 --- a/lib/pages/profile/profile_page.dart +++ b/lib/pages/profile/profile_page.dart @@ -5,27 +5,19 @@ import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'package:provider/provider.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:permission_handler/permission_handler.dart'; -import '../main.dart' show routeObserver; -import '../models/data_models.dart'; -import '../providers/app_provider.dart'; -import '../utils/user_prefs.dart'; -import '../utils/theme/app_theme.dart'; -import '../utils/toast_util.dart'; -import '../utils/database_helper.dart'; -import 'recycle_bin_page.dart'; -import 'sync/backup_page.dart'; -import '../widgets/fade_in_local_image.dart'; -import 'statistics_page.dart'; -import 'changelog_page.dart'; -import 'legal_page.dart'; -import 'online_search/enhanced_search_settings_page.dart'; -import 'sync/cloud_sync_page.dart'; -import 'app_icon_picker_page.dart'; -import 'tag_management_page.dart'; -import 'stroll_page.dart'; -import 'font_picker_page.dart'; +import '../../main.dart' show routeObserver; +import '../../models/data_models.dart'; +import '../../providers/app_provider.dart'; +import '../../utils/user_prefs.dart'; +import '../../utils/toast_util.dart'; +import '../settings/recycle_bin_page.dart'; +import '../sync/backup_page.dart'; +import '../../widgets/fade_in_local_image.dart'; +import '../explore/statistics_page.dart'; +import '../settings/tag_management_page.dart'; +import '../explore/stroll_page.dart'; +import '../sync/cloud_sync_page.dart'; +import 'settings_page.dart'; /// 个人中心页面 class ProfilePage extends StatefulWidget { @@ -1105,2086 +1097,3 @@ class _ProfilePageState extends State with RouteAware { ); } } - -// ─── 设置页面 ────────────────────────────────────────────────────────── - -class SettingsPage extends StatefulWidget { - const SettingsPage({super.key}); - - @override - State createState() => _SettingsPageState(); -} - -class _SettingsPageState extends State { - final UserPrefs _userPrefs = UserPrefs(); - bool _hideBottomNavOnScroll = true; - int _themeMode = 0; // 0=系统, 1=浅色, 2=深色 - String _fontFamily = ''; - - @override - void initState() { - super.initState(); - _hideBottomNavOnScroll = _userPrefs.hideBottomNavOnScroll; - _themeMode = _userPrefs.themeMode; - _fontFamily = _userPrefs.fontFamily; - } - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - return Scaffold( - backgroundColor: colors.surface, - appBar: AppBar(title: const Text('设置')), - body: ListView( - children: [ - _buildSectionHeader('显示设置'), - if (!Platform.isWindows) - _buildNavigationItem( - icon: Icons.apps_outlined, - title: '应用图标', - subtitle: '更换桌面应用图标', - onTap: () => Navigator.push(context, - MaterialPageRoute(builder: (_) => const AppIconPickerPage())), - ), - if (!Platform.isWindows) - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildNavigationItem( - icon: Icons.tune_outlined, - title: '功能设置', - subtitle: '启动标签、模块开关、侧边栏功能', - onTap: () => Navigator.push(context, - MaterialPageRoute(builder: (_) => const FeatureSettingsPage())), - ), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildNavigationItem( - icon: Icons.dashboard_outlined, - title: '布局设置', - subtitle: '影视、阅读、笔记的展示样式', - onTap: () => Navigator.push(context, - MaterialPageRoute(builder: (_) => const LayoutSettingsPage())), - ), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildThemeModeSelector(), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildColorSchemeSelector(), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildFontSelector(), - _buildSectionHeader('其他设置'), - _buildActionItem( - icon: Icons.person_outline, - title: '个人信息', - subtitle: '修改昵称和座右铭', - onTap: () => _showProfileEditDialog(context), - ), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildActionItem( - icon: Icons.manage_search, - title: '增强搜索', - subtitle: '在线搜索影视和书籍信息', - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const EnhancedSearchSettingsPage())), - ), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildSwitchItem( - icon: Icons.swipe_vertical_outlined, - title: '底部导航栏滚动隐藏', - subtitle: '下滑时自动隐藏底部导航栏', - value: _hideBottomNavOnScroll, - onChanged: _toggleHideBottomNavOnScroll, - ), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildSectionHeader('数据管理'), - _buildActionItem( - icon: Icons.cleaning_services_outlined, - title: '清除缓存数据', - subtitle: '清理未在数据库中引用的文件', - onTap: () => _showClearCacheDialog(context), - ), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildActionItem( - icon: Icons.folder_outlined, - title: '获取系统权限', - subtitle: '前往系统设置开启存储权限', - onTap: _showStoragePermissionDialog, - ), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildSectionHeader('帮助'), - _buildActionItem( - icon: Icons.language_outlined, - title: '查看官网', - subtitle: '在浏览器中打开官方网站', - onTap: () => - launchUrl(Uri.parse('https://mooknote.iletter.top/#/')), - ), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildActionItem( - icon: Icons.open_in_new_outlined, - title: '项目源码', - subtitle: '查看 GitHub 项目仓库', - onTap: () => - launchUrl(Uri.parse('https://github.com/dellevin/mooknote')), - ), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildActionItem( - icon: Icons.code_outlined, - title: '开发日志', - subtitle: '在浏览器中查看项目开发记录', - onTap: () => launchUrl(Uri.parse( - 'http://docmost.iletter.top/share/ropwljpyvn/p/mook-note-lHmPTswdDC')), - ), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildActionItem( - icon: Icons.update_outlined, - title: '更新日志', - subtitle: '查看版本更新内容', - onTap: () => Navigator.push(context, - MaterialPageRoute(builder: (_) => const ChangelogPage())), - ), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildActionItem( - icon: Icons.description_outlined, - title: '用户服务协议', - subtitle: '查看用户服务协议', - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => - const LegalPage(slug: 'terms', title: '用户服务协议'))), - ), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildActionItem( - icon: Icons.shield_outlined, - title: '隐私政策', - subtitle: '查看隐私政策', - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => - const LegalPage(slug: 'privacy', title: '隐私政策'))), - ), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - ], - ), - ); - } - - Future _toggleHideBottomNavOnScroll(bool value) async { - await _userPrefs.setHideBottomNavOnScroll(value); - setState(() => _hideBottomNavOnScroll = value); - } - - void _showProfileEditDialog(BuildContext context) { - final colors = Theme.of(context).colorScheme; - final nicknameController = TextEditingController(text: _userPrefs.nickname); - final mottoController = TextEditingController(text: _userPrefs.motto); - showDialog( - context: context, - builder: (ctx) => AlertDialog( - backgroundColor: colors.surface, - elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - title: Text('个人信息', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: colors.onSurface)), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: nicknameController, - style: TextStyle(fontSize: 14, color: colors.onSurface), - decoration: InputDecoration( - labelText: '昵称', - labelStyle: TextStyle( - fontSize: 13, - color: colors.onSurface.withValues(alpha: 0.5)), - filled: true, - fillColor: colors.surfaceContainerHighest, - contentPadding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none), - ), - ), - const SizedBox(height: 12), - TextField( - controller: mottoController, - maxLines: 2, - style: TextStyle(fontSize: 14, color: colors.onSurface), - decoration: InputDecoration( - labelText: '座右铭', - labelStyle: TextStyle( - fontSize: 13, - color: colors.onSurface.withValues(alpha: 0.5)), - filled: true, - fillColor: colors.surfaceContainerHighest, - contentPadding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none), - ), - ), - ], - ), - actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - style: TextButton.styleFrom( - foregroundColor: colors.onSurface.withValues(alpha: 0.6), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8)), - ), - child: const Text('取消', style: TextStyle(fontSize: 14)), - ), - ElevatedButton( - onPressed: () async { - final nickname = nicknameController.text.trim(); - final motto = mottoController.text.trim(); - if (nickname.isNotEmpty) await _userPrefs.setNickname(nickname); - if (motto.isNotEmpty) await _userPrefs.setMotto(motto); - if (ctx.mounted) Navigator.pop(ctx); - if (context.mounted) ToastUtil.show(context, '已保存'); - }, - style: ElevatedButton.styleFrom( - backgroundColor: colors.primary, - foregroundColor: colors.onPrimary, - elevation: 0, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8)), - ), - child: const Text('保存', style: TextStyle(fontSize: 14)), - ), - ], - ), - ); - } - - static const _themeModeLabels = ['跟随系统', '浅色模式', '深色模式']; - static const _themeModeIcons = [ - Icons.brightness_auto, - Icons.light_mode, - Icons.dark_mode - ]; - - Widget _buildThemeModeSelector() { - final colors = Theme.of(context).colorScheme; - return InkWell( - onTap: () => _showThemeModePicker(), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(10)), - child: Icon(_themeModeIcons[_themeMode], - color: colors.onSurface.withValues(alpha: 0.6), size: 18)), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('主题模式', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: colors.onSurface)), - const SizedBox(height: 2), - Text(_themeModeLabels[_themeMode], - style: TextStyle( - fontSize: 11, - color: colors.onSurface.withValues(alpha: 0.4))), - ]), - ), - Icon(Icons.chevron_right, - color: colors.onSurface.withValues(alpha: 0.25), size: 20), - ], - ), - ), - ); - } - - void _showThemeModePicker() { - final colors = Theme.of(context).colorScheme; - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (ctx) => Container( - decoration: BoxDecoration( - color: colors.surface, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(16))), - padding: const EdgeInsets.only(bottom: 12), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 36, - height: 4, - margin: const EdgeInsets.only(top: 12, bottom: 12), - decoration: BoxDecoration( - color: colors.onSurface.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(2))), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Align( - alignment: Alignment.centerLeft, - child: Text('主题模式', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: colors.onSurface))), - ), - const SizedBox(height: 8), - for (int i = 0; i < _themeModeLabels.length; i++) - InkWell( - onTap: () async { - await _setThemeMode(i); - Navigator.pop(ctx); - }, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 24, vertical: 10), - child: Row( - children: [ - Icon(_themeModeIcons[i], - size: 18, - color: colors.onSurface.withValues(alpha: 0.6)), - const SizedBox(width: 12), - Expanded( - child: Text(_themeModeLabels[i], - style: TextStyle( - fontSize: 13, color: colors.onSurface))), - if (_themeMode == i) - Icon(Icons.check, color: colors.onSurface, size: 18), - ], - ), - ), - ), - ], - ), - ), - ); - } - - Future _setThemeMode(int mode) async { - await _userPrefs.setThemeMode(mode); - final themeMode = switch (mode) { - 1 => ThemeMode.light, - 2 => ThemeMode.dark, - _ => ThemeMode.system, - }; - if (mounted) { - context.read().setThemeMode(themeMode); - setState(() => _themeMode = mode); - } - } - - Widget _buildColorSchemeSelector() { - final colors = Theme.of(context).colorScheme; - final provider = context.watch(); - final currentIndex = provider.colorSchemeIndex; - final label = - currentIndex == -1 ? '莫奈取色' : AppTheme.colorSchemeNames[currentIndex]; - return InkWell( - onTap: () => _showColorSchemePicker(), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(10)), - child: Icon( - currentIndex == -1 - ? Icons.auto_awesome - : Icons.palette_outlined, - color: colors.onSurface.withValues(alpha: 0.6), - size: 18)), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('配色方案', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: colors.onSurface)), - const SizedBox(height: 2), - Text(label, - style: TextStyle( - fontSize: 11, - color: colors.onSurface.withValues(alpha: 0.4))), - ]), - ), - Icon(Icons.chevron_right, - color: colors.onSurface.withValues(alpha: 0.25), size: 20), - ], - ), - ), - ); - } - - void _showColorSchemePicker() { - final colors = Theme.of(context).colorScheme; - final provider = context.read(); - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (ctx) => Container( - decoration: BoxDecoration( - color: colors.surface, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(16))), - padding: const EdgeInsets.only(bottom: 20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 36, - height: 4, - margin: const EdgeInsets.only(top: 12, bottom: 16), - decoration: BoxDecoration( - color: colors.onSurface.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(2))), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Align( - alignment: Alignment.centerLeft, - child: Text('配色方案', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: colors.onSurface))), - ), - const SizedBox(height: 12), - // 莫奈自动取色 - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: _buildMonetOption(provider, colors, ctx), - ), - const SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - mainAxisSpacing: 10, - crossAxisSpacing: 10, - childAspectRatio: 2.2), - itemCount: AppTheme.seedColors.length, - itemBuilder: (_, i) { - final selected = provider.colorSchemeIndex == i; - return GestureDetector( - onTap: () { - provider.setColorScheme(i); - Navigator.pop(ctx); - }, - child: Container( - decoration: BoxDecoration( - color: selected - ? AppTheme.seedColors[i].withValues(alpha: 0.12) - : colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: selected - ? AppTheme.seedColors[i] - : Colors.transparent, - width: 1.5), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 14, - height: 14, - decoration: BoxDecoration( - color: AppTheme.seedColors[i], - shape: BoxShape.circle)), - const SizedBox(width: 8), - Text(AppTheme.colorSchemeNames[i], - style: TextStyle( - fontSize: 13, - fontWeight: selected - ? FontWeight.w600 - : FontWeight.w400, - color: colors.onSurface)), - ], - ), - ), - ); - }, - ), - ), - ], - ), - ), - ); - } - - Widget _buildMonetOption( - AppProvider provider, ColorScheme colors, BuildContext ctx) { - final selected = provider.colorSchemeIndex == -1; - final monetColor = AppTheme.monetColor; - final available = monetColor != null; - return GestureDetector( - onTap: available - ? () { - provider.setColorScheme(-1); - Navigator.pop(ctx); - } - : null, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - decoration: BoxDecoration( - color: selected - ? (monetColor ?? colors.primary).withValues(alpha: 0.12) - : colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: selected - ? (monetColor ?? colors.primary) - : available - ? colors.outlineVariant - : colors.outlineVariant.withValues(alpha: 0.5), - width: selected ? 1.5 : 0.5), - ), - child: Row( - children: [ - Container( - width: 20, - height: 20, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: monetColor != null - ? LinearGradient( - colors: [monetColor, monetColor.withValues(alpha: 0.6)]) - : null, - color: available ? null : colors.outlineVariant, - ), - child: available - ? null - : Icon(Icons.auto_awesome, - size: 12, color: colors.onSurface.withValues(alpha: 0.3)), - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('莫奈取色', - style: TextStyle( - fontSize: 13, - fontWeight: - selected ? FontWeight.w600 : FontWeight.w500, - color: available - ? colors.onSurface - : colors.onSurface.withValues(alpha: 0.35))), - Text(available ? '从系统壁纸自动提取配色' : '此设备不支持', - style: TextStyle( - fontSize: 11, - color: colors.onSurface.withValues(alpha: 0.35))), - ]), - ), - if (selected) - Icon(Icons.check_circle, - size: 18, color: monetColor ?? colors.primary), - ], - ), - ), - ); - } - - // ─── 字体选择器 ─── - - Widget _buildFontSelector() { - final colors = Theme.of(context).colorScheme; - final label = _fontFamily.isEmpty ? '系统默认' : _fontFamily; - return InkWell( - onTap: () async { - final result = await Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const FontPickerPage(initialFamily: '')), - ); - if (result != null && mounted) { - _setFontFamily(result); - } - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Row(children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(10)), - child: Icon(Icons.font_download_outlined, - size: 18, color: colors.onSurface.withValues(alpha: 0.6))), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('字体', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: colors.onSurface)), - const SizedBox(height: 2), - Text(label, - style: TextStyle( - fontSize: 11, - color: colors.onSurface.withValues(alpha: 0.4))), - ])), - Icon(Icons.chevron_right, - size: 20, color: colors.onSurface.withValues(alpha: 0.25)), - ]), - ), - ); - } - - void _setFontFamily(String family) { - setState(() => _fontFamily = family); - context.read().setFontFamily(family); - } - - Widget _buildSwitchItem( - {required IconData icon, - required String title, - required String subtitle, - required bool value, - required ValueChanged onChanged}) { - final colors = Theme.of(context).colorScheme; - return InkWell( - onTap: () => onChanged(!value), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(10)), - child: Icon(icon, - color: colors.onSurface.withValues(alpha: 0.6), size: 18)), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: colors.onSurface)), - const SizedBox(height: 2), - Text(subtitle, - style: TextStyle( - fontSize: 11, - color: colors.onSurface.withValues(alpha: 0.4))), - ]), - ), - Switch( - value: value, - onChanged: onChanged, - activeColor: colors.primary, - activeTrackColor: colors.primary.withValues(alpha: 0.3), - inactiveThumbColor: colors.surface, - inactiveTrackColor: colors.outline), - ], - ), - ), - ); - } - - Widget _buildSectionHeader(String title) { - final colors = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.fromLTRB(24, 20, 24, 8), - child: Text(title, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: colors.onSurface)), - ); - } - - Widget _buildNavigationItem( - {required IconData icon, - required String title, - required String subtitle, - required VoidCallback onTap}) { - final colors = Theme.of(context).colorScheme; - return InkWell( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(10)), - child: Icon(icon, - color: colors.onSurface.withValues(alpha: 0.6), size: 18)), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: colors.onSurface)), - const SizedBox(height: 2), - Text(subtitle, - style: TextStyle( - fontSize: 11, - color: colors.onSurface.withValues(alpha: 0.4))), - ]), - ), - Icon(Icons.chevron_right, - color: colors.onSurface.withValues(alpha: 0.25), size: 20), - ], - ), - ), - ); - } - - Widget _buildActionItem( - {required IconData icon, - required String title, - required String subtitle, - required VoidCallback onTap}) { - final colors = Theme.of(context).colorScheme; - return InkWell( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(10)), - child: Icon(icon, - color: colors.onSurface.withValues(alpha: 0.6), size: 18)), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: colors.onSurface)), - const SizedBox(height: 2), - Text(subtitle, - style: TextStyle( - fontSize: 11, - color: colors.onSurface.withValues(alpha: 0.4))), - ]), - ), - ], - ), - ), - ); - } - - void _showStoragePermissionDialog() { - final colors = Theme.of(context).colorScheme; - showDialog( - context: context, - builder: (ctx) => AlertDialog( - backgroundColor: colors.surface, - elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - title: Text('需要存储权限', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: colors.onSurface)), - content: Text( - 'Android 11+ 需要在系统设置中授予"所有文件访问权限"才能扫描字体文件。\n\n是否前往设置?', - style: TextStyle( - fontSize: 14, - color: colors.onSurface.withValues(alpha: 0.6), - height: 1.6), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: Text('取消', - style: - TextStyle(color: colors.onSurface.withValues(alpha: 0.4))), - ), - TextButton( - onPressed: () { - Navigator.pop(ctx); - // 跳转到应用设置页(用户可在权限中找到"所有文件访问") - openAppSettings(); - }, - child: Text('前往设置', style: TextStyle(color: colors.primary)), - ), - ], - ), - ); - } - - void _showClearCacheDialog(BuildContext pageContext) async { - final colors = Theme.of(context).colorScheme; - // 先扫描分析 - showDialog( - context: pageContext, - barrierDismissible: false, - builder: (_) => Center(child: CircularProgressIndicator(color: colors.primary)), - ); - - final appProvider = pageContext.read(); - final dbImagePaths = await _getAllDbImagePaths(appProvider); - - final imageInfo = await _scanImageDirectory(dbImagePaths); - final epubInfo = await _scanOrphanedEpubBooks(appProvider); - final tempInfo = await _scanTempDirectory(); - final emptyDirInfo = await _scanEmptyDirectories(); - - if (!pageContext.mounted) return; - Navigator.pop(pageContext); // 关闭 loading - - final totalSize = imageInfo.$2 + epubInfo.$2 + tempInfo.$2 + emptyDirInfo.$2; - final totalCount = imageInfo.$1 + epubInfo.$1 + tempInfo.$1 + emptyDirInfo.$1; - if (totalCount == 0) { - ToastUtil.show(pageContext, '没有需要清理的缓存'); - return; - } - - showDialog( - context: pageContext, - builder: (dialogContext) => AlertDialog( - backgroundColor: colors.surface, - elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - title: Text('缓存分析', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('共发现 $totalCount 项可清理缓存,合计 ${_formatSize(totalSize)}', - style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))), - const SizedBox(height: 14), - if (imageInfo.$1 > 0) _buildCacheItem('孤立图片', imageInfo.$1, imageInfo.$2, Icons.image_outlined, colors), - if (epubInfo.$1 > 0) _buildCacheItem('孤立电子书', epubInfo.$1, epubInfo.$2, Icons.menu_book_outlined, colors), - if (tempInfo.$1 > 0) _buildCacheItem('临时文件', tempInfo.$1, tempInfo.$2, Icons.folder_outlined, colors), - if (emptyDirInfo.$1 > 0) _buildCacheItem('空文件夹', emptyDirInfo.$1, emptyDirInfo.$2, Icons.folder_off_outlined, colors), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))), - ElevatedButton( - onPressed: () async { - Navigator.pop(dialogContext); - await _clearCacheData(pageContext); - }, - style: ElevatedButton.styleFrom( - backgroundColor: colors.error, - foregroundColor: colors.onError, - elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)), - child: const Text('确认清除'), - ), - ], - actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - ), - ); - } - - Widget _buildCacheItem(String label, int count, int size, IconData icon, ColorScheme colors) { - return Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Row( - children: [ - Icon(icon, size: 18, color: colors.onSurface.withValues(alpha: 0.4)), - const SizedBox(width: 10), - Expanded( - child: Text(label, - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface)), - ), - Text('$count项 ${_formatSize(size)}', - style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))), - ], - ), - ); - } - - String _formatSize(int bytes) { - if (bytes < 1024) return '$bytes B'; - if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; - return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; - } - - Future _clearCacheData(BuildContext context) async { - try { - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const Center(child: CircularProgressIndicator())); - final appProvider = context.read(); - - // 1. 清理未引用的图片文件 - final dbImagePaths = await _getAllDbImagePaths(appProvider); - final deletedImages = await _cleanImageDirectory(dbImagePaths); - - // 2. 清理孤立的 epub_books 目录 - final deletedEpubs = await _cleanOrphanedEpubBooks(appProvider); - - // 3. 清理临时目录缓存(分享海报、备份ZIP等) - final deletedTemp = await _cleanTempDirectory(); - - // 4. 清理空文件夹 - final deletedEmptyDirs = await _cleanEmptyDirectories(); - - Navigator.pop(context); - if (context.mounted) { - final total = - deletedImages + deletedEpubs + deletedTemp + deletedEmptyDirs; - if (total == 0) { - ToastUtil.show(context, '没有需要清理的缓存'); - } else { - ToastUtil.show(context, - '已清理 $deletedImages 个孤立图片,$deletedEpubs 个孤立电子书,$deletedTemp 个临时文件,$deletedEmptyDirs 个空文件夹'); - } - } - } catch (e) { - Navigator.pop(context); - if (context.mounted) ToastUtil.show(context, '清理失败: $e'); - } - } - - Future> _getAllDbImagePaths(AppProvider provider) async { - final paths = {}; - for (final movie in provider.movies) { - if (movie.posterPath?.isNotEmpty == true) paths.add(movie.posterPath!); - } - for (final book in provider.books) { - if (book.coverPath?.isNotEmpty == true) paths.add(book.coverPath!); - } - for (final note in provider.notes) { - for (final p in note.images) { - if (p.isNotEmpty) paths.add(p); - } - } - for (final movieId in provider.movies.map((m) => m.id)) { - for (final poster in await provider.getMoviePosters(movieId)) { - if (poster.posterPath.isNotEmpty) paths.add(poster.posterPath); - } - } - return paths; - } - - Future _cleanImageDirectory(Set dbImagePaths) async { - int deletedCount = 0; - try { - final appDir = await getApplicationDocumentsDirectory(); - final imagesDir = Directory('${appDir.path}/images'); - if (!await imagesDir.exists()) return 0; - await for (final entity - in imagesDir.list(recursive: true, followLinks: false)) { - if (entity is File && - !dbImagePaths.contains(entity.path) && - !path.basename(entity.path).startsWith('avatar')) { - try { - await entity.delete(); - deletedCount++; - } catch (_) {} - } - } - } catch (e) { - debugPrint('清理图片目录失败: $e'); - } - return deletedCount; - } - - /// 清理 epub_books 中孤立的目录(数据库中不存在的) - Future _cleanOrphanedEpubBooks(AppProvider provider) async { - int deletedCount = 0; - try { - final db = await DatabaseHelper.instance.database; - // 收集数据库中所有引用的 epub_books 子目录名(包括软删除的) - final rows = await db.query('reader_books', - columns: ['id', 'file_path', 'cover_path', 'is_deleted']); - final usedDirs = {}; - for (final r in rows) { - // 只收集未删除的记录对应的目录 - final isDeleted = r['is_deleted'] == 1 || r['is_deleted'] == true; - if (isDeleted) continue; - final id = r['id'] as String?; - if (id != null && id.isNotEmpty) usedDirs.add(id); - _collectEpubDirName(r['file_path'] as String?, usedDirs); - _collectEpubDirName(r['cover_path'] as String?, usedDirs); - } - - // 检查 /data/user/0/top.iletter.mooknote/app_flutter/epub_books 路径 - final appDir = await getApplicationDocumentsDirectory(); - final possiblePaths = [ - '${appDir.path}/epub_books', - '/data/user/0/top.iletter.mooknote/app_flutter/epub_books', - ]; - - for (final epubPath in possiblePaths) { - final epubDir = Directory(epubPath); - if (!await epubDir.exists()) continue; - - await for (final entity in epubDir.list(followLinks: false)) { - if (entity is Directory) { - final dirName = path.basename(entity.path); - if (!usedDirs.contains(dirName)) { - try { - await entity.delete(recursive: true); - deletedCount++; - } catch (_) {} - } - } - } - } - } catch (e) { - debugPrint('清理 epub_books 目录失败: $e'); - } - return deletedCount; - } - - /// 从绝对路径中提取 epub_books/ 下的目录名 - void _collectEpubDirName(String? pathStr, Set dirs) { - if (pathStr == null || pathStr.isEmpty) return; - final marker = '/epub_books/'; - final idx = pathStr.indexOf(marker); - if (idx < 0) return; - final rest = pathStr.substring(idx + marker.length); - final slashIdx = rest.indexOf('/'); - dirs.add(slashIdx >= 0 ? rest.substring(0, slashIdx) : rest); - } - - Future _cleanTempDirectory() async { - int deletedCount = 0; - final now = DateTime.now(); - - // 1. 清理临时目录中的分享海报(保留备份ZIP) - try { - final tempDir = await getTemporaryDirectory(); - if (await tempDir.exists()) { - await for (final entity in tempDir.list(followLinks: false)) { - if (entity is File) { - final name = path.basename(entity.path); - if (name.startsWith('book_poster_') || - name.startsWith('movie_poster_') || - name.startsWith('note_share_') || - name.startsWith('mooknote_download') || - name.startsWith('mooknote_bidir')) { - try { - final stat = await entity.stat(); - if (now.difference(stat.modified).inHours >= 1) { - await entity.delete(); - deletedCount++; - } - } catch (_) {} - } - } - } - } - } catch (e) { - debugPrint('清理临时目录失败: $e'); - } - - // 2. 清理应用缓存目录 (/data/user/0/{package}/cache/) - try { - final cacheDir = await getApplicationCacheDirectory(); - if (await cacheDir.exists()) { - await for (final entity - in cacheDir.list(recursive: true, followLinks: false)) { - if (entity is File) { - try { - await entity.delete(); - deletedCount++; - } catch (_) {} - } - } - } - } catch (e) { - debugPrint('清理缓存目录失败: $e'); - } - - return deletedCount; - } - - /// 清理 images、epub_books、cache 下的空文件夹 - Future _cleanEmptyDirectories() async { - int deletedCount = 0; - try { - final appDir = await getApplicationDocumentsDirectory(); - final cacheDir = await getApplicationCacheDirectory(); - final dirs = [ - Directory('${appDir.path}/images'), - Directory('${appDir.path}/epub_books'), - cacheDir, - ]; - for (final dir in dirs) { - if (!await dir.exists()) continue; - deletedCount += await _removeEmptyDirsRecursive(dir); - } - } catch (e) { - debugPrint('清理空文件夹失败: $e'); - } - return deletedCount; - } - - /// 递归删除空子文件夹(自底向上),不删除根目录本身 - Future _removeEmptyDirsRecursive(Directory dir) async { - int count = 0; - try { - final children = await dir.list(followLinks: false).toList(); - for (final child in children) { - if (child is Directory) { - count += await _removeEmptyDirsRecursive(child); - final remaining = await child.list(followLinks: false).toList(); - if (remaining.isEmpty) { - try { - await child.delete(); - count++; - } catch (_) {} - } - } - } - } catch (_) {} - return count; - } - - // ─── 扫描方法(只统计不删除) ────────────────────────────────────────────── - - /// 返回 (文件数, 总字节数) - Future<(int, int)> _scanImageDirectory(Set dbImagePaths) async { - int count = 0, totalSize = 0; - try { - final appDir = await getApplicationDocumentsDirectory(); - final imagesDir = Directory('${appDir.path}/images'); - if (!await imagesDir.exists()) return (0, 0); - await for (final entity in imagesDir.list(recursive: true, followLinks: false)) { - if (entity is File && - !dbImagePaths.contains(entity.path) && - !path.basename(entity.path).startsWith('avatar')) { - try { - totalSize += await entity.length(); - count++; - } catch (_) {} - } - } - } catch (_) {} - return (count, totalSize); - } - - Future<(int, int)> _scanOrphanedEpubBooks(AppProvider provider) async { - int count = 0, totalSize = 0; - try { - final db = await DatabaseHelper.instance.database; - final rows = await db.query('reader_books', columns: ['id', 'file_path', 'cover_path', 'is_deleted']); - final usedDirs = {}; - for (final r in rows) { - final isDeleted = r['is_deleted'] == 1 || r['is_deleted'] == true; - if (isDeleted) continue; - final id = r['id'] as String?; - if (id != null && id.isNotEmpty) usedDirs.add(id); - _collectEpubDirName(r['file_path'] as String?, usedDirs); - _collectEpubDirName(r['cover_path'] as String?, usedDirs); - } - final appDir = await getApplicationDocumentsDirectory(); - final possiblePaths = [ - '${appDir.path}/epub_books', - '/data/user/0/top.iletter.mooknote/app_flutter/epub_books', - ]; - for (final epubPath in possiblePaths) { - final epubDir = Directory(epubPath); - if (!await epubDir.exists()) continue; - await for (final entity in epubDir.list(followLinks: false)) { - if (entity is Directory) { - final dirName = path.basename(entity.path); - if (!usedDirs.contains(dirName)) { - try { - totalSize += await _dirSize(entity); - count++; - } catch (_) {} - } - } - } - } - } catch (_) {} - return (count, totalSize); - } - - Future<(int, int)> _scanTempDirectory() async { - int count = 0, totalSize = 0; - final now = DateTime.now(); - try { - final tempDir = await getTemporaryDirectory(); - if (await tempDir.exists()) { - await for (final entity in tempDir.list(followLinks: false)) { - if (entity is File) { - final name = path.basename(entity.path); - if (name.startsWith('book_poster_') || - name.startsWith('movie_poster_') || - name.startsWith('note_share_') || - name.startsWith('mooknote_download') || - name.startsWith('mooknote_bidir')) { - try { - final stat = await entity.stat(); - if (now.difference(stat.modified).inHours >= 1) { - totalSize += await entity.length(); - count++; - } - } catch (_) {} - } - } - } - } - } catch (_) {} - try { - final cacheDir = await getApplicationCacheDirectory(); - if (await cacheDir.exists()) { - await for (final entity in cacheDir.list(recursive: true, followLinks: false)) { - if (entity is File) { - try { - totalSize += await entity.length(); - count++; - } catch (_) {} - } - } - } - } catch (_) {} - return (count, totalSize); - } - - Future<(int, int)> _scanEmptyDirectories() async { - int count = 0; - try { - final appDir = await getApplicationDocumentsDirectory(); - final cacheDir = await getApplicationCacheDirectory(); - final dirs = [ - Directory('${appDir.path}/images'), - Directory('${appDir.path}/epub_books'), - cacheDir, - ]; - for (final dir in dirs) { - if (!await dir.exists()) continue; - count += await _countEmptyDirsRecursive(dir); - } - } catch (_) {} - return (count, 0); - } - - Future _dirSize(Directory dir) async { - int size = 0; - try { - await for (final entity in dir.list(recursive: true, followLinks: false)) { - if (entity is File) { - try { size += await entity.length(); } catch (_) {} - } - } - } catch (_) {} - return size; - } - - Future _countEmptyDirsRecursive(Directory dir) async { - int count = 0; - try { - final children = await dir.list(followLinks: false).toList(); - for (final child in children) { - if (child is Directory) { - count += await _countEmptyDirsRecursive(child); - final remaining = await child.list(followLinks: false).toList(); - if (remaining.isEmpty) count++; - } - } - } catch (_) {} - return count; - } -} - -// ─── 功能设置 ─── - -class FeatureSettingsPage extends StatefulWidget { - const FeatureSettingsPage({super.key}); - - @override - State createState() => _FeatureSettingsPageState(); -} - -class _FeatureSettingsPageState extends State { - final UserPrefs _userPrefs = UserPrefs(); - - // 主界面 - bool _showMovieTab = true; - bool _showBookTab = true; - bool _showNoteTab = true; - int _defaultTabIndex = 0; - - // 侧边栏 - bool _showHeatmap = true; - bool _showRecent = true; - bool _showEncounter = true; - bool _showStroll = true; - bool _showCalendar = true; - bool _showPerson = true; - bool _showTags = true; - bool _showMdReader = true; - bool _showEpub = true; - - @override - void initState() { - super.initState(); - _loadSettings(); - } - - void _loadSettings() { - setState(() { - _showMovieTab = _userPrefs.showMovieTab; - _showBookTab = _userPrefs.showBookTab; - _showNoteTab = _userPrefs.showNoteTab; - _defaultTabIndex = _userPrefs.defaultMainTabIndex; - _showHeatmap = _userPrefs.showSidebarHeatmap; - _showRecent = _userPrefs.showSidebarRecent; - _showEncounter = _userPrefs.showSidebarEncounter; - _showStroll = _userPrefs.showSidebarStroll; - _showCalendar = _userPrefs.showSidebarCalendar; - _showPerson = _userPrefs.showSidebarPerson; - _showTags = _userPrefs.showSidebarTags; - _showMdReader = _userPrefs.showSidebarMdReader; - _showEpub = _userPrefs.showSidebarEpub; - }); - } - - int get _enabledTabCount { - int count = 0; - if (_showMovieTab) count++; - if (_showBookTab) count++; - if (_showNoteTab) count++; - return count; - } - - List<(int, String, IconData)> get _enabledTabs { - final all = [ - (0, '影视', Icons.movie_outlined), - (1, '阅读', Icons.menu_book_outlined), - (2, '笔记', Icons.note_outlined), - ]; - return all.where((t) { - return switch (t.$1) { - 0 => _showMovieTab, - 1 => _showBookTab, - 2 => _showNoteTab, - _ => false, - }; - }).toList(); - } - - void _fixDefaultTabIndex() { - final enabled = _enabledTabs; - if (!enabled.any((t) => t.$1 == _defaultTabIndex) && enabled.isNotEmpty) { - _defaultTabIndex = enabled.first.$1; - _userPrefs.setDefaultMainTabIndex(_defaultTabIndex); - } - } - - Future _toggleMovieTab(bool value) async { - if (!value && _enabledTabCount <= 1) { - ToastUtil.show(context, '至少保留一个标签页'); - return; - } - await _userPrefs.setShowMovieTab(value); - setState(() { - _showMovieTab = value; - _fixDefaultTabIndex(); - }); - } - - Future _toggleBookTab(bool value) async { - if (!value && _enabledTabCount <= 1) { - ToastUtil.show(context, '至少保留一个标签页'); - return; - } - await _userPrefs.setShowBookTab(value); - setState(() { - _showBookTab = value; - _fixDefaultTabIndex(); - }); - } - - Future _toggleNoteTab(bool value) async { - if (!value && _enabledTabCount <= 1) { - ToastUtil.show(context, '至少保留一个标签页'); - return; - } - await _userPrefs.setShowNoteTab(value); - setState(() { - _showNoteTab = value; - _fixDefaultTabIndex(); - }); - } - - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - return Scaffold( - backgroundColor: colors.surface, - appBar: AppBar(title: const Text('功能设置')), - body: ListView( - children: [ - // ── 启动设置 ── - _buildSectionHeader('启动设置'), - _buildDefaultTabSelector(), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - - // ── 模块开关 ── - _buildSectionHeader('模块开关'), - _buildSwitchItem(Icons.movie_outlined, '观影', '记录和管理观影记录', - _showMovieTab, _toggleMovieTab), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildSwitchItem(Icons.menu_book_outlined, '阅读', '记录和管理阅读记录', - _showBookTab, _toggleBookTab), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildSwitchItem(Icons.note_outlined, '笔记', '记录和管理笔记', _showNoteTab, - _toggleNoteTab), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - // ── 侧边栏:信息模块 ── - _buildSectionHeader('侧边栏 · 信息模块'), - _buildSwitchItem( - Icons.calendar_today, '热力图', '显示创作活跃度热力图', _showHeatmap, - (v) async { - await _userPrefs.setShowSidebarHeatmap(v); - setState(() => _showHeatmap = v); - }), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildSwitchItem(Icons.schedule, '最近添加', '显示最近添加的记录', _showRecent, - (v) async { - await _userPrefs.setShowSidebarRecent(v); - setState(() => _showRecent = v); - }), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildSwitchItem( - Icons.favorite_border, '统计', '与应用相遇的天数和数据概览', _showEncounter, - (v) async { - await _userPrefs.setShowSidebarEncounter(v); - setState(() => _showEncounter = v); - }), - - // ── 侧边栏:快捷功能 ── - _buildSectionHeader('侧边栏 · 快捷功能'), - _buildSwitchItem(Icons.explore_outlined, '漫步', '随机发现内容', _showStroll, - (v) async { - await _userPrefs.setShowSidebarStroll(v); - setState(() => _showStroll = v); - }), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildSwitchItem( - Icons.calendar_month_outlined, '书影日历', '按日历查看记录', _showCalendar, - (v) async { - await _userPrefs.setShowSidebarCalendar(v); - setState(() => _showCalendar = v); - }), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildSwitchItem( - Icons.people_outline, '角色信息', '管理影视和书籍中的角色', _showPerson, - (v) async { - await _userPrefs.setShowSidebarPerson(v); - setState(() => _showPerson = v); - }), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildSwitchItem(Icons.label_outline, '标签管理', '管理所有标签', _showTags, - (v) async { - await _userPrefs.setShowSidebarTags(v); - setState(() => _showTags = v); - }), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildSwitchItem(Icons.description_outlined, 'MD阅读', 'Markdown 文件阅读器', - _showMdReader, (v) async { - await _userPrefs.setShowSidebarMdReader(v); - setState(() => _showMdReader = v); - }), - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - _buildSwitchItem( - Icons.auto_stories_outlined, 'EPUB阅读', 'EPUB 电子书阅读器', _showEpub, - (v) async { - await _userPrefs.setShowSidebarEpub(v); - setState(() => _showEpub = v); - }), - Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - child: Text('关闭后对应功能将从界面中隐藏。', - style: TextStyle( - fontSize: 11, - color: colors.onSurface.withValues(alpha: 0.3))), - ), - ], - ), - ); - } - - Widget _buildSectionHeader(String title) { - final colors = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.fromLTRB(24, 20, 24, 8), - child: Text(title, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: colors.onSurface)), - ); - } - - Widget _buildSwitchItem(IconData icon, String title, String subtitle, - bool value, ValueChanged onChanged) { - final colors = Theme.of(context).colorScheme; - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), - leading: Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(10)), - child: Icon(icon, - color: colors.onSurface.withValues(alpha: 0.6), size: 18)), - title: Text(title, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: colors.onSurface)), - subtitle: Text(subtitle, - style: TextStyle( - fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), - trailing: Switch( - value: value, - onChanged: onChanged, - activeColor: colors.primary, - activeTrackColor: colors.primary.withValues(alpha: 0.3), - inactiveThumbColor: colors.surface, - inactiveTrackColor: colors.outline), - ); - } - - Widget _buildDefaultTabSelector() { - final colors = Theme.of(context).colorScheme; - final enabled = _enabledTabs; - final currentLabel = enabled - .firstWhere((t) => t.$1 == _defaultTabIndex, - orElse: () => enabled.first) - .$2; - - return InkWell( - onTap: _showDefaultTabPicker, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - child: Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(10)), - child: Icon(Icons.home_outlined, - color: colors.onSurface.withValues(alpha: 0.6), size: 18)), - const SizedBox(width: 12), - Expanded( - child: Text('默认启动标签', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: colors.onSurface)), - ), - Text(currentLabel, - style: TextStyle( - fontSize: 13, - color: colors.onSurface.withValues(alpha: 0.5))), - const SizedBox(width: 4), - Icon(Icons.chevron_right, - size: 18, color: colors.onSurface.withValues(alpha: 0.25)), - ], - ), - ), - ); - } - - void _showDefaultTabPicker() { - final colors = Theme.of(context).colorScheme; - final enabled = _enabledTabs; - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (ctx) => Container( - decoration: BoxDecoration( - color: colors.surface, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(16))), - padding: const EdgeInsets.symmetric(vertical: 12), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 36, - height: 4, - decoration: BoxDecoration( - color: colors.onSurface.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(2))), - const SizedBox(height: 20), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Text('默认启动标签', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: colors.onSurface)))), - const SizedBox(height: 16), - for (final t in enabled) - ListTile( - leading: Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(10)), - child: Icon(t.$3, - color: colors.onSurface.withValues(alpha: 0.6))), - title: Text(t.$2, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: colors.onSurface)), - trailing: _defaultTabIndex == t.$1 - ? Icon(Icons.check, color: colors.onSurface, size: 20) - : null, - onTap: () async { - await _userPrefs.setDefaultMainTabIndex(t.$1); - setState(() => _defaultTabIndex = t.$1); - Navigator.pop(ctx); - }, - ), - const SizedBox(height: 8), - ], - ), - ), - ); - } -} - -// ─── 布局设置 ────────────────────────────────────────────────────────── - -class LayoutSettingsPage extends StatefulWidget { - const LayoutSettingsPage({super.key}); - - @override - State createState() => _LayoutSettingsPageState(); -} - -class _LayoutSettingsPageState extends State { - final UserPrefs _userPrefs = UserPrefs(); - int _noteLayout = 0; - int _movieLayout = 0; - int _bookLayout = 0; - bool _movieWallMode = false; - bool _bookshelfMode = false; - - @override - void initState() { - super.initState(); - _noteLayout = _userPrefs.noteLayoutStyle; - _movieLayout = _userPrefs.movieLayoutStyle; - _bookLayout = _userPrefs.bookLayoutStyle; - _movieWallMode = _userPrefs.movieWallMode; - _bookshelfMode = _userPrefs.bookshelfMode; - } - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - return Scaffold( - backgroundColor: colors.surface, - appBar: AppBar(title: const Text('布局设置')), - body: ListView( - padding: const EdgeInsets.symmetric(vertical: 16), - children: [ - // ── 影视 ── - _buildCategoryHeader(Icons.movie_outlined, '影视', colors.primary), - _buildWallSwitch( - icon: Icons.wallpaper_outlined, - title: '影视墙模式', - subtitle: '显示全部影片,不区分状态', - value: _movieWallMode, - onChanged: (v) => _setWallMode('movie', v), - ), - _buildLayoutSelector( - selected: _movieLayout, - options: [ - (0, Icons.grid_view_outlined, '海报网格'), - (1, Icons.view_list_outlined, '列表'), - (2, Icons.crop_landscape_outlined, '大图卡片'), - ], - onChanged: (v) => _setLayout('movie', v), - ), - const SizedBox(height: 8), - Divider( - height: 1, - indent: 16, - endIndent: 16, - color: colors.outlineVariant.withValues(alpha: 0.5)), - const SizedBox(height: 8), - - // ── 阅读 ── - _buildCategoryHeader(Icons.menu_book_outlined, '阅读', colors.primary), - _buildWallSwitch( - icon: Icons.auto_stories_outlined, - title: '书架模式', - subtitle: '显示全部书籍,不区分状态', - value: _bookshelfMode, - onChanged: (v) => _setWallMode('book', v), - ), - _buildLayoutSelector( - selected: _bookLayout, - options: [ - (0, Icons.grid_view_outlined, '海报网格'), - (1, Icons.view_list_outlined, '列表'), - ], - onChanged: (v) => _setLayout('book', v), - ), - const SizedBox(height: 8), - Divider( - height: 1, - indent: 16, - endIndent: 16, - color: colors.outlineVariant.withValues(alpha: 0.5)), - const SizedBox(height: 8), - - // ── 笔记 ── - _buildCategoryHeader(Icons.note_outlined, '笔记', colors.primary), - _buildLayoutSelector( - selected: _noteLayout, - options: [ - (0, Icons.view_list_outlined, '列表'), - (1, Icons.grid_view_outlined, '瀑布流'), - (2, Icons.timeline_outlined, '时间线'), - ], - onChanged: (v) => _setLayout('note', v), - ), - ], - ), - ); - } - - Widget _buildCategoryHeader(IconData icon, String title, Color color) { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), - child: Row( - children: [ - Container( - width: 28, - height: 28, - decoration: BoxDecoration( - color: color.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon(icon, size: 16, color: color), - ), - const SizedBox(width: 10), - Text(title, - style: TextStyle( - fontSize: 15, fontWeight: FontWeight.w700, color: color)), - ], - ), - ); - } - - Widget _buildWallSwitch({ - required IconData icon, - required String title, - required String subtitle, - required bool value, - required ValueChanged onChanged, - }) { - final colors = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - decoration: BoxDecoration( - color: colors.surfaceContainerHighest.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: colors.primary.withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(10), - ), - child: Icon(icon, - size: 20, color: colors.primary.withValues(alpha: 0.8)), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: colors.onSurface)), - const SizedBox(height: 2), - Text(subtitle, - style: TextStyle( - fontSize: 12, - color: colors.onSurface.withValues(alpha: 0.5))), - ], - ), - ), - Switch( - value: value, - onChanged: onChanged, - activeThumbColor: colors.primary, - ), - ], - ), - ), - ); - } - - void _setWallMode(String type, bool value) async { - switch (type) { - case 'movie': - await _userPrefs.setMovieWallMode(value); - setState(() => _movieWallMode = value); - if (mounted) context.read().setMovieWallMode(value); - case 'book': - await _userPrefs.setBookshelfMode(value); - setState(() => _bookshelfMode = value); - if (mounted) context.read().setBookshelfMode(value); - } - } - - void _setLayout(String type, int value) async { - switch (type) { - case 'note': - await _userPrefs.setNoteLayoutStyle(value); - setState(() => _noteLayout = value); - case 'movie': - await _userPrefs.setMovieLayoutStyle(value); - setState(() => _movieLayout = value); - if (mounted) context.read().setMovieLayoutStyle(value); - case 'book': - await _userPrefs.setBookLayoutStyle(value); - setState(() => _bookLayout = value); - } - } - - Widget _buildLayoutSelector({ - required int selected, - required List<(int, IconData, String)> options, - required ValueChanged onChanged, - }) { - final colors = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), - child: Row( - children: options.map((opt) { - final isSelected = selected == opt.$1; - return Expanded( - child: GestureDetector( - onTap: () => onChanged(opt.$1), - child: AnimatedContainer( - duration: const Duration(milliseconds: 150), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.symmetric(vertical: 14), - decoration: BoxDecoration( - color: isSelected - ? colors.primary - : colors.surfaceContainerHighest.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isSelected - ? colors.primary - : colors.outlineVariant.withValues(alpha: 0.5), - width: isSelected ? 0 : 0.5, - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - opt.$2, - size: 22, - color: isSelected - ? colors.onPrimary - : colors.onSurface.withValues(alpha: 0.5), - ), - const SizedBox(height: 6), - Text( - opt.$3, - style: TextStyle( - fontSize: 12, - fontWeight: - isSelected ? FontWeight.w600 : FontWeight.w500, - color: isSelected - ? colors.onPrimary - : colors.onSurface.withValues(alpha: 0.5), - ), - ), - ], - ), - ), - ), - ); - }).toList(), - ), - ); - } -} diff --git a/lib/pages/profile/settings_page.dart b/lib/pages/profile/settings_page.dart new file mode 100644 index 0000000..f672bc3 --- /dev/null +++ b/lib/pages/profile/settings_page.dart @@ -0,0 +1,1414 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; +import 'package:provider/provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../../providers/app_provider.dart'; +import '../../utils/user_prefs.dart'; +import '../../utils/theme/app_theme.dart'; +import '../../utils/toast_util.dart'; +import '../../data/database_helper.dart'; +import '../online_search/enhanced_search_settings_page.dart'; +import '../settings/legal_page.dart'; +import 'app_icon_picker_page.dart'; +import 'feature_settings_page.dart'; +import 'layout_settings_page.dart'; +import 'changelog_page.dart'; +import 'font_picker_page.dart'; + +/// 设置页面 +class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + final UserPrefs _userPrefs = UserPrefs(); + bool _hideBottomNavOnScroll = true; + int _themeMode = 0; // 0=系统, 1=浅色, 2=深色 + String _fontFamily = ''; + + @override + void initState() { + super.initState(); + _hideBottomNavOnScroll = _userPrefs.hideBottomNavOnScroll; + _themeMode = _userPrefs.themeMode; + _fontFamily = _userPrefs.fontFamily; + } + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; + return Scaffold( + backgroundColor: colors.surface, + appBar: AppBar(title: const Text('设置')), + body: ListView( + children: [ + _buildSectionHeader('显示设置'), + if (!Platform.isWindows) + _buildNavigationItem( + icon: Icons.apps_outlined, + title: '应用图标', + subtitle: '更换桌面应用图标', + onTap: () => Navigator.push(context, + MaterialPageRoute(builder: (_) => const AppIconPickerPage())), + ), + if (!Platform.isWindows) + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildNavigationItem( + icon: Icons.tune_outlined, + title: '功能设置', + subtitle: '启动标签、模块开关、侧边栏功能', + onTap: () => Navigator.push(context, + MaterialPageRoute(builder: (_) => const FeatureSettingsPage())), + ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildNavigationItem( + icon: Icons.dashboard_outlined, + title: '布局设置', + subtitle: '影视、阅读、笔记的展示样式', + onTap: () => Navigator.push(context, + MaterialPageRoute(builder: (_) => const LayoutSettingsPage())), + ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildThemeModeSelector(), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildColorSchemeSelector(), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildFontSelector(), + _buildSectionHeader('其他设置'), + _buildActionItem( + icon: Icons.person_outline, + title: '个人信息', + subtitle: '修改昵称和座右铭', + onTap: () => _showProfileEditDialog(context), + ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildActionItem( + icon: Icons.manage_search, + title: '增强搜索', + subtitle: '在线搜索影视和书籍信息', + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const EnhancedSearchSettingsPage())), + ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem( + icon: Icons.swipe_vertical_outlined, + title: '底部导航栏滚动隐藏', + subtitle: '下滑时自动隐藏底部导航栏', + value: _hideBottomNavOnScroll, + onChanged: _toggleHideBottomNavOnScroll, + ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSectionHeader('数据管理'), + _buildActionItem( + icon: Icons.cleaning_services_outlined, + title: '清除缓存数据', + subtitle: '清理未在数据库中引用的文件', + onTap: () => _showClearCacheDialog(context), + ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildActionItem( + icon: Icons.folder_outlined, + title: '获取系统权限', + subtitle: '前往系统设置开启存储权限', + onTap: _showStoragePermissionDialog, + ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSectionHeader('帮助'), + _buildActionItem( + icon: Icons.language_outlined, + title: '查看官网', + subtitle: '在浏览器中打开官方网站', + onTap: () => + launchUrl(Uri.parse('https://mooknote.iletter.top/#/')), + ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildActionItem( + icon: Icons.open_in_new_outlined, + title: '项目源码', + subtitle: '查看 GitHub 项目仓库', + onTap: () => + launchUrl(Uri.parse('https://github.com/dellevin/mooknote')), + ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildActionItem( + icon: Icons.code_outlined, + title: '开发日志', + subtitle: '在浏览器中查看项目开发记录', + onTap: () => launchUrl(Uri.parse( + 'http://docmost.iletter.top/share/ropwljpyvn/p/mook-note-lHmPTswdDC')), + ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildActionItem( + icon: Icons.update_outlined, + title: '更新日志', + subtitle: '查看版本更新内容', + onTap: () => Navigator.push(context, + MaterialPageRoute(builder: (_) => const ChangelogPage())), + ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildActionItem( + icon: Icons.description_outlined, + title: '用户服务协议', + subtitle: '查看用户服务协议', + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + const LegalPage(slug: 'terms', title: '用户服务协议'))), + ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildActionItem( + icon: Icons.shield_outlined, + title: '隐私政策', + subtitle: '查看隐私政策', + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + const LegalPage(slug: 'privacy', title: '隐私政策'))), + ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + ], + ), + ); + } + + Future _toggleHideBottomNavOnScroll(bool value) async { + await _userPrefs.setHideBottomNavOnScroll(value); + setState(() => _hideBottomNavOnScroll = value); + } + + void _showProfileEditDialog(BuildContext context) { + final colors = Theme.of(context).colorScheme; + final nicknameController = TextEditingController(text: _userPrefs.nickname); + final mottoController = TextEditingController(text: _userPrefs.motto); + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: colors.surface, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Text('个人信息', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: colors.onSurface)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nicknameController, + style: TextStyle(fontSize: 14, color: colors.onSurface), + decoration: InputDecoration( + labelText: '昵称', + labelStyle: TextStyle( + fontSize: 13, + color: colors.onSurface.withValues(alpha: 0.5)), + filled: true, + fillColor: colors.surfaceContainerHighest, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none), + ), + ), + const SizedBox(height: 12), + TextField( + controller: mottoController, + maxLines: 2, + style: TextStyle(fontSize: 14, color: colors.onSurface), + decoration: InputDecoration( + labelText: '座右铭', + labelStyle: TextStyle( + fontSize: 13, + color: colors.onSurface.withValues(alpha: 0.5)), + filled: true, + fillColor: colors.surfaceContainerHighest, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none), + ), + ), + ], + ), + actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + style: TextButton.styleFrom( + foregroundColor: colors.onSurface.withValues(alpha: 0.6), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), + ), + child: const Text('取消', style: TextStyle(fontSize: 14)), + ), + ElevatedButton( + onPressed: () async { + final nickname = nicknameController.text.trim(); + final motto = mottoController.text.trim(); + if (nickname.isNotEmpty) await _userPrefs.setNickname(nickname); + if (motto.isNotEmpty) await _userPrefs.setMotto(motto); + if (ctx.mounted) Navigator.pop(ctx); + if (context.mounted) ToastUtil.show(context, '已保存'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: colors.primary, + foregroundColor: colors.onPrimary, + elevation: 0, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), + ), + child: const Text('保存', style: TextStyle(fontSize: 14)), + ), + ], + ), + ); + } + + static const _themeModeLabels = ['跟随系统', '浅色模式', '深色模式']; + static const _themeModeIcons = [ + Icons.brightness_auto, + Icons.light_mode, + Icons.dark_mode + ]; + + Widget _buildThemeModeSelector() { + final colors = Theme.of(context).colorScheme; + return InkWell( + onTap: () => _showThemeModePicker(), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10)), + child: Icon(_themeModeIcons[_themeMode], + color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('主题模式', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: colors.onSurface)), + const SizedBox(height: 2), + Text(_themeModeLabels[_themeMode], + style: TextStyle( + fontSize: 11, + color: colors.onSurface.withValues(alpha: 0.4))), + ]), + ), + Icon(Icons.chevron_right, + color: colors.onSurface.withValues(alpha: 0.25), size: 20), + ], + ), + ), + ); + } + + void _showThemeModePicker() { + final colors = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (ctx) => Container( + decoration: BoxDecoration( + color: colors.surface, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(16))), + padding: const EdgeInsets.only(bottom: 12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 36, + height: 4, + margin: const EdgeInsets.only(top: 12, bottom: 12), + decoration: BoxDecoration( + color: colors.onSurface.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(2))), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Align( + alignment: Alignment.centerLeft, + child: Text('主题模式', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colors.onSurface))), + ), + const SizedBox(height: 8), + for (int i = 0; i < _themeModeLabels.length; i++) + InkWell( + onTap: () async { + await _setThemeMode(i); + Navigator.pop(ctx); + }, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 10), + child: Row( + children: [ + Icon(_themeModeIcons[i], + size: 18, + color: colors.onSurface.withValues(alpha: 0.6)), + const SizedBox(width: 12), + Expanded( + child: Text(_themeModeLabels[i], + style: TextStyle( + fontSize: 13, color: colors.onSurface))), + if (_themeMode == i) + Icon(Icons.check, color: colors.onSurface, size: 18), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Future _setThemeMode(int mode) async { + await _userPrefs.setThemeMode(mode); + final themeMode = switch (mode) { + 1 => ThemeMode.light, + 2 => ThemeMode.dark, + _ => ThemeMode.system, + }; + if (mounted) { + context.read().setThemeMode(themeMode); + setState(() => _themeMode = mode); + } + } + + Widget _buildColorSchemeSelector() { + final colors = Theme.of(context).colorScheme; + final provider = context.watch(); + final currentIndex = provider.colorSchemeIndex; + final label = + currentIndex == -1 ? '莫奈取色' : AppTheme.colorSchemeNames[currentIndex]; + return InkWell( + onTap: () => _showColorSchemePicker(), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10)), + child: Icon( + currentIndex == -1 + ? Icons.auto_awesome + : Icons.palette_outlined, + color: colors.onSurface.withValues(alpha: 0.6), + size: 18)), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('配色方案', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: colors.onSurface)), + const SizedBox(height: 2), + Text(label, + style: TextStyle( + fontSize: 11, + color: colors.onSurface.withValues(alpha: 0.4))), + ]), + ), + Icon(Icons.chevron_right, + color: colors.onSurface.withValues(alpha: 0.25), size: 20), + ], + ), + ), + ); + } + + void _showColorSchemePicker() { + final colors = Theme.of(context).colorScheme; + final provider = context.read(); + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (ctx) => Container( + decoration: BoxDecoration( + color: colors.surface, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(16))), + padding: const EdgeInsets.only(bottom: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 36, + height: 4, + margin: const EdgeInsets.only(top: 12, bottom: 16), + decoration: BoxDecoration( + color: colors.onSurface.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(2))), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Align( + alignment: Alignment.centerLeft, + child: Text('配色方案', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colors.onSurface))), + ), + const SizedBox(height: 12), + // 莫奈自动取色 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: _buildMonetOption(provider, colors, ctx), + ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 2.2), + itemCount: AppTheme.seedColors.length, + itemBuilder: (_, i) { + final selected = provider.colorSchemeIndex == i; + return GestureDetector( + onTap: () { + provider.setColorScheme(i); + Navigator.pop(ctx); + }, + child: Container( + decoration: BoxDecoration( + color: selected + ? AppTheme.seedColors[i].withValues(alpha: 0.12) + : colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: selected + ? AppTheme.seedColors[i] + : Colors.transparent, + width: 1.5), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: AppTheme.seedColors[i], + shape: BoxShape.circle)), + const SizedBox(width: 8), + Text(AppTheme.colorSchemeNames[i], + style: TextStyle( + fontSize: 13, + fontWeight: selected + ? FontWeight.w600 + : FontWeight.w400, + color: colors.onSurface)), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ), + ); + } + + Widget _buildMonetOption( + AppProvider provider, ColorScheme colors, BuildContext ctx) { + final selected = provider.colorSchemeIndex == -1; + final monetColor = AppTheme.monetColor; + final available = monetColor != null; + return GestureDetector( + onTap: available + ? () { + provider.setColorScheme(-1); + Navigator.pop(ctx); + } + : null, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: selected + ? (monetColor ?? colors.primary).withValues(alpha: 0.12) + : colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: selected + ? (monetColor ?? colors.primary) + : available + ? colors.outlineVariant + : colors.outlineVariant.withValues(alpha: 0.5), + width: selected ? 1.5 : 0.5), + ), + child: Row( + children: [ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: monetColor != null + ? LinearGradient( + colors: [monetColor, monetColor.withValues(alpha: 0.6)]) + : null, + color: available ? null : colors.outlineVariant, + ), + child: available + ? null + : Icon(Icons.auto_awesome, + size: 12, color: colors.onSurface.withValues(alpha: 0.3)), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('莫奈取色', + style: TextStyle( + fontSize: 13, + fontWeight: + selected ? FontWeight.w600 : FontWeight.w500, + color: available + ? colors.onSurface + : colors.onSurface.withValues(alpha: 0.35))), + Text(available ? '从系统壁纸自动提取配色' : '此设备不支持', + style: TextStyle( + fontSize: 11, + color: colors.onSurface.withValues(alpha: 0.35))), + ]), + ), + if (selected) + Icon(Icons.check_circle, + size: 18, color: monetColor ?? colors.primary), + ], + ), + ), + ); + } + + // ─── 字体选择器 ─── + + Widget _buildFontSelector() { + final colors = Theme.of(context).colorScheme; + final label = _fontFamily.isEmpty ? '系统默认' : _fontFamily; + return InkWell( + onTap: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const FontPickerPage(initialFamily: '')), + ); + if (result != null && mounted) { + _setFontFamily(result); + } + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Row(children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10)), + child: Icon(Icons.font_download_outlined, + size: 18, color: colors.onSurface.withValues(alpha: 0.6))), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('字体', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: colors.onSurface)), + const SizedBox(height: 2), + Text(label, + style: TextStyle( + fontSize: 11, + color: colors.onSurface.withValues(alpha: 0.4))), + ])), + Icon(Icons.chevron_right, + size: 20, color: colors.onSurface.withValues(alpha: 0.25)), + ]), + ), + ); + } + + void _setFontFamily(String family) { + setState(() => _fontFamily = family); + context.read().setFontFamily(family); + } + + Widget _buildSwitchItem( + {required IconData icon, + required String title, + required String subtitle, + required bool value, + required ValueChanged onChanged}) { + final colors = Theme.of(context).colorScheme; + return InkWell( + onTap: () => onChanged(!value), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10)), + child: Icon(icon, + color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: colors.onSurface)), + const SizedBox(height: 2), + Text(subtitle, + style: TextStyle( + fontSize: 11, + color: colors.onSurface.withValues(alpha: 0.4))), + ]), + ), + Switch( + value: value, + onChanged: onChanged, + activeColor: colors.primary, + activeTrackColor: colors.primary.withValues(alpha: 0.3), + inactiveThumbColor: colors.surface, + inactiveTrackColor: colors.outline), + ], + ), + ), + ); + } + + Widget _buildSectionHeader(String title) { + final colors = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.fromLTRB(24, 20, 24, 8), + child: Text(title, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: colors.onSurface)), + ); + } + + Widget _buildNavigationItem( + {required IconData icon, + required String title, + required String subtitle, + required VoidCallback onTap}) { + final colors = Theme.of(context).colorScheme; + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10)), + child: Icon(icon, + color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: colors.onSurface)), + const SizedBox(height: 2), + Text(subtitle, + style: TextStyle( + fontSize: 11, + color: colors.onSurface.withValues(alpha: 0.4))), + ]), + ), + Icon(Icons.chevron_right, + color: colors.onSurface.withValues(alpha: 0.25), size: 20), + ], + ), + ), + ); + } + + Widget _buildActionItem( + {required IconData icon, + required String title, + required String subtitle, + required VoidCallback onTap}) { + final colors = Theme.of(context).colorScheme; + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10)), + child: Icon(icon, + color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: colors.onSurface)), + const SizedBox(height: 2), + Text(subtitle, + style: TextStyle( + fontSize: 11, + color: colors.onSurface.withValues(alpha: 0.4))), + ]), + ), + ], + ), + ), + ); + } + + void _showStoragePermissionDialog() { + final colors = Theme.of(context).colorScheme; + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: colors.surface, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Text('需要存储权限', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colors.onSurface)), + content: Text( + 'Android 11+ 需要在系统设置中授予"所有文件访问权限"才能扫描字体文件。\n\n是否前往设置?', + style: TextStyle( + fontSize: 14, + color: colors.onSurface.withValues(alpha: 0.6), + height: 1.6), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text('取消', + style: + TextStyle(color: colors.onSurface.withValues(alpha: 0.4))), + ), + TextButton( + onPressed: () { + Navigator.pop(ctx); + // 跳转到应用设置页(用户可在权限中找到"所有文件访问") + openAppSettings(); + }, + child: Text('前往设置', style: TextStyle(color: colors.primary)), + ), + ], + ), + ); + } + + void _showClearCacheDialog(BuildContext pageContext) async { + final colors = Theme.of(context).colorScheme; + // 先扫描分析 + showDialog( + context: pageContext, + barrierDismissible: false, + builder: (_) => Center(child: CircularProgressIndicator(color: colors.primary)), + ); + + final appProvider = pageContext.read(); + final dbImagePaths = await _getAllDbImagePaths(appProvider); + + final imageInfo = await _scanImageDirectory(dbImagePaths); + final epubInfo = await _scanOrphanedEpubBooks(appProvider); + final tempInfo = await _scanTempDirectory(); + final emptyDirInfo = await _scanEmptyDirectories(); + + if (!pageContext.mounted) return; + Navigator.pop(pageContext); // 关闭 loading + + final totalSize = imageInfo.$2 + epubInfo.$2 + tempInfo.$2 + emptyDirInfo.$2; + final totalCount = imageInfo.$1 + epubInfo.$1 + tempInfo.$1 + emptyDirInfo.$1; + if (totalCount == 0) { + ToastUtil.show(pageContext, '没有需要清理的缓存'); + return; + } + + showDialog( + context: pageContext, + builder: (dialogContext) => AlertDialog( + backgroundColor: colors.surface, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Text('缓存分析', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('共发现 $totalCount 项可清理缓存,合计 ${_formatSize(totalSize)}', + style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))), + const SizedBox(height: 14), + if (imageInfo.$1 > 0) _buildCacheItem('孤立图片', imageInfo.$1, imageInfo.$2, Icons.image_outlined, colors), + if (epubInfo.$1 > 0) _buildCacheItem('孤立电子书', epubInfo.$1, epubInfo.$2, Icons.menu_book_outlined, colors), + if (tempInfo.$1 > 0) _buildCacheItem('临时文件', tempInfo.$1, tempInfo.$2, Icons.folder_outlined, colors), + if (emptyDirInfo.$1 > 0) _buildCacheItem('空文件夹', emptyDirInfo.$1, emptyDirInfo.$2, Icons.folder_off_outlined, colors), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))), + ElevatedButton( + onPressed: () async { + Navigator.pop(dialogContext); + await _clearCacheData(pageContext); + }, + style: ElevatedButton.styleFrom( + backgroundColor: colors.error, + foregroundColor: colors.onError, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)), + child: const Text('确认清除'), + ), + ], + actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ); + } + + Widget _buildCacheItem(String label, int count, int size, IconData icon, ColorScheme colors) { + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + Icon(icon, size: 18, color: colors.onSurface.withValues(alpha: 0.4)), + const SizedBox(width: 10), + Expanded( + child: Text(label, + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface)), + ), + Text('$count项 ${_formatSize(size)}', + style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))), + ], + ), + ); + } + + String _formatSize(int bytes) { + if (bytes < 1024) return '$bytes B'; + if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; + return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; + } + + Future _clearCacheData(BuildContext context) async { + try { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const Center(child: CircularProgressIndicator())); + final appProvider = context.read(); + + // 1. 清理未引用的图片文件 + final dbImagePaths = await _getAllDbImagePaths(appProvider); + final deletedImages = await _cleanImageDirectory(dbImagePaths); + + // 2. 清理孤立的 epub_books 目录 + final deletedEpubs = await _cleanOrphanedEpubBooks(appProvider); + + // 3. 清理临时目录缓存(分享海报、备份ZIP等) + final deletedTemp = await _cleanTempDirectory(); + + // 4. 清理空文件夹 + final deletedEmptyDirs = await _cleanEmptyDirectories(); + + Navigator.pop(context); + if (context.mounted) { + final total = + deletedImages + deletedEpubs + deletedTemp + deletedEmptyDirs; + if (total == 0) { + ToastUtil.show(context, '没有需要清理的缓存'); + } else { + ToastUtil.show(context, + '已清理 $deletedImages 个孤立图片,$deletedEpubs 个孤立电子书,$deletedTemp 个临时文件,$deletedEmptyDirs 个空文件夹'); + } + } + } catch (e) { + Navigator.pop(context); + if (context.mounted) ToastUtil.show(context, '清理失败: $e'); + } + } + + Future> _getAllDbImagePaths(AppProvider provider) async { + final paths = {}; + for (final movie in provider.movies) { + if (movie.posterPath?.isNotEmpty == true) paths.add(movie.posterPath!); + } + for (final book in provider.books) { + if (book.coverPath?.isNotEmpty == true) paths.add(book.coverPath!); + } + for (final note in provider.notes) { + for (final p in note.images) { + if (p.isNotEmpty) paths.add(p); + } + } + for (final movieId in provider.movies.map((m) => m.id)) { + for (final poster in await provider.getMoviePosters(movieId)) { + if (poster.posterPath.isNotEmpty) paths.add(poster.posterPath); + } + } + return paths; + } + + Future _cleanImageDirectory(Set dbImagePaths) async { + int deletedCount = 0; + try { + final appDir = await getApplicationDocumentsDirectory(); + final imagesDir = Directory('${appDir.path}/images'); + if (!await imagesDir.exists()) return 0; + await for (final entity + in imagesDir.list(recursive: true, followLinks: false)) { + if (entity is File && + !dbImagePaths.contains(entity.path) && + !path.basename(entity.path).startsWith('avatar')) { + try { + await entity.delete(); + deletedCount++; + } catch (_) {} + } + } + } catch (e) { + debugPrint('清理图片目录失败: $e'); + } + return deletedCount; + } + + /// 清理 epub_books 中孤立的目录(数据库中不存在的) + Future _cleanOrphanedEpubBooks(AppProvider provider) async { + int deletedCount = 0; + try { + final db = await DatabaseHelper.instance.database; + // 收集数据库中所有引用的 epub_books 子目录名(包括软删除的) + final rows = await db.query('reader_books', + columns: ['id', 'file_path', 'cover_path', 'is_deleted']); + final usedDirs = {}; + for (final r in rows) { + // 只收集未删除的记录对应的目录 + final isDeleted = r['is_deleted'] == 1 || r['is_deleted'] == true; + if (isDeleted) continue; + final id = r['id'] as String?; + if (id != null && id.isNotEmpty) usedDirs.add(id); + _collectEpubDirName(r['file_path'] as String?, usedDirs); + _collectEpubDirName(r['cover_path'] as String?, usedDirs); + } + + // 检查 /data/user/0/top.iletter.mooknote/app_flutter/epub_books 路径 + final appDir = await getApplicationDocumentsDirectory(); + final possiblePaths = [ + '${appDir.path}/epub_books', + '/data/user/0/top.iletter.mooknote/app_flutter/epub_books', + ]; + + for (final epubPath in possiblePaths) { + final epubDir = Directory(epubPath); + if (!await epubDir.exists()) continue; + + await for (final entity in epubDir.list(followLinks: false)) { + if (entity is Directory) { + final dirName = path.basename(entity.path); + if (!usedDirs.contains(dirName)) { + try { + await entity.delete(recursive: true); + deletedCount++; + } catch (_) {} + } + } + } + } + } catch (e) { + debugPrint('清理 epub_books 目录失败: $e'); + } + return deletedCount; + } + + /// 从绝对路径中提取 epub_books/ 下的目录名 + void _collectEpubDirName(String? pathStr, Set dirs) { + if (pathStr == null || pathStr.isEmpty) return; + final marker = '/epub_books/'; + final idx = pathStr.indexOf(marker); + if (idx < 0) return; + final rest = pathStr.substring(idx + marker.length); + final slashIdx = rest.indexOf('/'); + dirs.add(slashIdx >= 0 ? rest.substring(0, slashIdx) : rest); + } + + Future _cleanTempDirectory() async { + int deletedCount = 0; + final now = DateTime.now(); + + // 1. 清理临时目录中的分享海报(保留备份ZIP) + try { + final tempDir = await getTemporaryDirectory(); + if (await tempDir.exists()) { + await for (final entity in tempDir.list(followLinks: false)) { + if (entity is File) { + final name = path.basename(entity.path); + if (name.startsWith('book_poster_') || + name.startsWith('movie_poster_') || + name.startsWith('note_share_') || + name.startsWith('mooknote_download') || + name.startsWith('mooknote_bidir')) { + try { + final stat = await entity.stat(); + if (now.difference(stat.modified).inHours >= 1) { + await entity.delete(); + deletedCount++; + } + } catch (_) {} + } + } + } + } + } catch (e) { + debugPrint('清理临时目录失败: $e'); + } + + // 2. 清理应用缓存目录 (/data/user/0/{package}/cache/) + try { + final cacheDir = await getApplicationCacheDirectory(); + if (await cacheDir.exists()) { + await for (final entity + in cacheDir.list(recursive: true, followLinks: false)) { + if (entity is File) { + try { + await entity.delete(); + deletedCount++; + } catch (_) {} + } + } + } + } catch (e) { + debugPrint('清理缓存目录失败: $e'); + } + + return deletedCount; + } + + /// 清理 images、epub_books、cache 下的空文件夹 + Future _cleanEmptyDirectories() async { + int deletedCount = 0; + try { + final appDir = await getApplicationDocumentsDirectory(); + final cacheDir = await getApplicationCacheDirectory(); + final dirs = [ + Directory('${appDir.path}/images'), + Directory('${appDir.path}/epub_books'), + cacheDir, + ]; + for (final dir in dirs) { + if (!await dir.exists()) continue; + deletedCount += await _removeEmptyDirsRecursive(dir); + } + } catch (e) { + debugPrint('清理空文件夹失败: $e'); + } + return deletedCount; + } + + /// 递归删除空子文件夹(自底向上),不删除根目录本身 + Future _removeEmptyDirsRecursive(Directory dir) async { + int count = 0; + try { + final children = await dir.list(followLinks: false).toList(); + for (final child in children) { + if (child is Directory) { + count += await _removeEmptyDirsRecursive(child); + final remaining = await child.list(followLinks: false).toList(); + if (remaining.isEmpty) { + try { + await child.delete(); + count++; + } catch (_) {} + } + } + } + } catch (_) {} + return count; + } + + // ─── 扫描方法(只统计不删除) ────────────────────────────────────────────── + + /// 返回 (文件数, 总字节数) + Future<(int, int)> _scanImageDirectory(Set dbImagePaths) async { + int count = 0, totalSize = 0; + try { + final appDir = await getApplicationDocumentsDirectory(); + final imagesDir = Directory('${appDir.path}/images'); + if (!await imagesDir.exists()) return (0, 0); + await for (final entity in imagesDir.list(recursive: true, followLinks: false)) { + if (entity is File && + !dbImagePaths.contains(entity.path) && + !path.basename(entity.path).startsWith('avatar')) { + try { + totalSize += await entity.length(); + count++; + } catch (_) {} + } + } + } catch (_) {} + return (count, totalSize); + } + + Future<(int, int)> _scanOrphanedEpubBooks(AppProvider provider) async { + int count = 0, totalSize = 0; + try { + final db = await DatabaseHelper.instance.database; + final rows = await db.query('reader_books', columns: ['id', 'file_path', 'cover_path', 'is_deleted']); + final usedDirs = {}; + for (final r in rows) { + final isDeleted = r['is_deleted'] == 1 || r['is_deleted'] == true; + if (isDeleted) continue; + final id = r['id'] as String?; + if (id != null && id.isNotEmpty) usedDirs.add(id); + _collectEpubDirName(r['file_path'] as String?, usedDirs); + _collectEpubDirName(r['cover_path'] as String?, usedDirs); + } + final appDir = await getApplicationDocumentsDirectory(); + final possiblePaths = [ + '${appDir.path}/epub_books', + '/data/user/0/top.iletter.mooknote/app_flutter/epub_books', + ]; + for (final epubPath in possiblePaths) { + final epubDir = Directory(epubPath); + if (!await epubDir.exists()) continue; + await for (final entity in epubDir.list(followLinks: false)) { + if (entity is Directory) { + final dirName = path.basename(entity.path); + if (!usedDirs.contains(dirName)) { + try { + totalSize += await _dirSize(entity); + count++; + } catch (_) {} + } + } + } + } + } catch (_) {} + return (count, totalSize); + } + + Future<(int, int)> _scanTempDirectory() async { + int count = 0, totalSize = 0; + final now = DateTime.now(); + try { + final tempDir = await getTemporaryDirectory(); + if (await tempDir.exists()) { + await for (final entity in tempDir.list(followLinks: false)) { + if (entity is File) { + final name = path.basename(entity.path); + if (name.startsWith('book_poster_') || + name.startsWith('movie_poster_') || + name.startsWith('note_share_') || + name.startsWith('mooknote_download') || + name.startsWith('mooknote_bidir')) { + try { + final stat = await entity.stat(); + if (now.difference(stat.modified).inHours >= 1) { + totalSize += await entity.length(); + count++; + } + } catch (_) {} + } + } + } + } + } catch (_) {} + try { + final cacheDir = await getApplicationCacheDirectory(); + if (await cacheDir.exists()) { + await for (final entity in cacheDir.list(recursive: true, followLinks: false)) { + if (entity is File) { + try { + totalSize += await entity.length(); + count++; + } catch (_) {} + } + } + } + } catch (_) {} + return (count, totalSize); + } + + Future<(int, int)> _scanEmptyDirectories() async { + int count = 0; + try { + final appDir = await getApplicationDocumentsDirectory(); + final cacheDir = await getApplicationCacheDirectory(); + final dirs = [ + Directory('${appDir.path}/images'), + Directory('${appDir.path}/epub_books'), + cacheDir, + ]; + for (final dir in dirs) { + if (!await dir.exists()) continue; + count += await _countEmptyDirsRecursive(dir); + } + } catch (_) {} + return (count, 0); + } + + Future _dirSize(Directory dir) async { + int size = 0; + try { + await for (final entity in dir.list(recursive: true, followLinks: false)) { + if (entity is File) { + try { size += await entity.length(); } catch (_) {} + } + } + } catch (_) {} + return size; + } + + Future _countEmptyDirsRecursive(Directory dir) async { + int count = 0; + try { + final children = await dir.list(followLinks: false).toList(); + for (final child in children) { + if (child is Directory) { + count += await _countEmptyDirsRecursive(child); + final remaining = await child.list(followLinks: false).toList(); + if (remaining.isEmpty) count++; + } + } + } catch (_) {} + return count; + } +} diff --git a/lib/pages/settings/legal_page.dart b/lib/pages/settings/legal_page.dart index 631cfbb..333c463 100644 --- a/lib/pages/settings/legal_page.dart +++ b/lib/pages/settings/legal_page.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:http/http.dart' as http; -import '../utils/server_config.dart'; +import '../../utils/server_config.dart'; /// 用户服务协议 / 隐私政策查看页面 class LegalPage extends StatefulWidget { diff --git a/lib/pages/settings/recycle_bin_page.dart b/lib/pages/settings/recycle_bin_page.dart index 545254d..1949c4e 100644 --- a/lib/pages/settings/recycle_bin_page.dart +++ b/lib/pages/settings/recycle_bin_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../providers/app_provider.dart'; -import '../models/data_models.dart'; -import '../utils/toast_util.dart'; +import '../../providers/app_provider.dart'; +import '../../models/data_models.dart'; +import '../../utils/toast_util.dart'; /// 回收站页面 class RecycleBinPage extends StatefulWidget { diff --git a/lib/pages/settings/tag_management_page.dart b/lib/pages/settings/tag_management_page.dart index 289ccbe..31d18e9 100644 --- a/lib/pages/settings/tag_management_page.dart +++ b/lib/pages/settings/tag_management_page.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../providers/app_provider.dart'; -import '../utils/toast_util.dart'; +import '../../providers/app_provider.dart'; +import '../../utils/toast_util.dart'; class TagManagementPage extends StatefulWidget { const TagManagementPage({super.key}); diff --git a/lib/pages/sync/backup_page.dart b/lib/pages/sync/backup_page.dart index 83c19ba..cdf3981 100644 --- a/lib/pages/sync/backup_page.dart +++ b/lib/pages/sync/backup_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../providers/app_provider.dart'; -import '../../utils/sync/backup_service.dart'; -import '../../utils/sync/auto_backup_service.dart'; +import '../../services/sync/backup_service.dart'; +import '../../services/sync/auto_backup_service.dart'; import '../../utils/toast_util.dart'; /// 本地备份页面 diff --git a/lib/pages/sync/webdav_sync_page.dart b/lib/pages/sync/webdav_sync_page.dart index bc5a224..88e82b1 100644 --- a/lib/pages/sync/webdav_sync_page.dart +++ b/lib/pages/sync/webdav_sync_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../utils/toast_util.dart'; -import '../../utils/sync/webdav_service.dart'; +import '../../services/sync/webdav_service.dart'; import '../../providers/app_provider.dart'; /// WebDAV 备份页面 diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index c92ba9c..9e012e1 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -1,19 +1,19 @@ import 'dart:collection'; import 'package:flutter/material.dart'; import '../models/data_models.dart'; -import '../utils/movie/movie_dao.dart'; -import '../utils/book/book_dao.dart'; -import '../utils/note/note_dao.dart'; -import '../utils/movie/movie_review_dao.dart'; -import '../utils/movie/movie_poster_dao.dart'; -import '../utils/book/book_review_dao.dart'; -import '../utils/book/book_excerpt_dao.dart'; -import '../utils/tag/tag_dao.dart'; -import '../utils/database_helper.dart'; +import '../data/movie/movie_dao.dart'; +import '../data/book/book_dao.dart'; +import '../data/note/note_dao.dart'; +import '../data/movie/movie_review_dao.dart'; +import '../data/movie/movie_poster_dao.dart'; +import '../data/book/book_review_dao.dart'; +import '../data/book/book_excerpt_dao.dart'; +import '../data/tag/tag_dao.dart'; +import '../data/database_helper.dart'; import '../utils/image_path_helper.dart'; import '../utils/user_prefs.dart'; import '../utils/theme/app_theme.dart'; -import '../utils/font_download_manager.dart'; +import '../services/font_download_manager.dart'; /// 应用全局状态管理 class AppProvider extends ChangeNotifier { diff --git a/lib/services/changelog_service.dart b/lib/services/changelog_service.dart index 992ca1e..5e63d38 100644 --- a/lib/services/changelog_service.dart +++ b/lib/services/changelog_service.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; -import 'server_config.dart'; +import '../utils/server_config.dart'; /// 更新日志数据模型 class ChangelogItem { diff --git a/lib/services/epub/epub_parser.dart b/lib/services/epub/epub_parser.dart index 132e361..9e06407 100644 --- a/lib/services/epub/epub_parser.dart +++ b/lib/services/epub/epub_parser.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:archive/archive.dart'; import 'package:flutter/foundation.dart'; import 'package:xml/xml.dart'; -import 'reader_models.dart'; +import '../../data/epub/reader_models.dart'; /// EPUB 解析器 - 从 ZIP 归档中解析 EPUB 结构 class EpubParser { diff --git a/lib/services/epub/epub_service.dart b/lib/services/epub/epub_service.dart index d3aeee5..7d6cb7e 100644 --- a/lib/services/epub/epub_service.dart +++ b/lib/services/epub/epub_service.dart @@ -4,8 +4,8 @@ import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; import 'package:uuid/uuid.dart'; import 'epub_parser.dart'; -import 'reader_dao.dart'; -import 'reader_models.dart'; +import '../../data/epub/reader_dao.dart'; +import '../../data/epub/reader_models.dart'; /// EPUB 服务层 - 管理导入、解压、删除 class EpubService { diff --git a/lib/services/sync/backup_service.dart b/lib/services/sync/backup_service.dart index f1995f4..bb1ed62 100644 --- a/lib/services/sync/backup_service.dart +++ b/lib/services/sync/backup_service.dart @@ -8,8 +8,8 @@ import 'package:path/path.dart' as path; import 'package:share_plus/share_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite/sqflite.dart'; -import '../database_helper.dart'; -import '../user_prefs.dart'; +import '../../data/database_helper.dart'; +import '../../utils/user_prefs.dart'; /// 数据备份服务 - 支持导出和导入数据(包含图片) class BackupService { diff --git a/lib/services/usage_stats_service.dart b/lib/services/usage_stats_service.dart index fc930e8..78dcdc0 100644 --- a/lib/services/usage_stats_service.dart +++ b/lib/services/usage_stats_service.dart @@ -6,12 +6,12 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; -import 'user_prefs.dart'; -import 'server_config.dart'; +import '../utils/user_prefs.dart'; +import '../utils/server_config.dart'; /// 匿名用户统计服务(静默运行,对用户不可见) /// -/// App 启动后每 5 分钟向统计服务器发送匿名心跳。 +/// App 启动后每 1 分钟向统计服务器发送匿名心跳。 /// 统计数据在服务端管理后台查看,App 内无入口。 class UsageStatsService with WidgetsBindingObserver { static final UsageStatsService instance = UsageStatsService._(); diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index bebdef4..385f513 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -3,13 +3,13 @@ import 'package:provider/provider.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../providers/app_provider.dart'; import '../utils/user_prefs.dart'; -import '../pages/encounter_page.dart'; -import '../pages/stroll_page.dart'; -import '../pages/media_calendar_page.dart'; -import '../pages/person_list_page.dart'; +import '../pages/explore/encounter_page.dart'; +import '../pages/explore/stroll_page.dart'; +import '../pages/explore/media_calendar_page.dart'; +import '../pages/explore/person_list_page.dart'; import '../pages/markdown_reader/md_reader_tab_page.dart'; import '../pages/epub_reader/epub_library_page.dart'; -import '../pages/tag_management_page.dart'; +import '../pages/settings/tag_management_page.dart'; import '../pages/movies/movie_detail_page.dart'; import '../pages/book/book_detail_page.dart'; import '../pages/note/note_detail_page.dart';