From 5022a13b744c8aaeff87c846c4b69d83264089c8 Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Wed, 27 May 2026 14:09:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E8=B0=83=E4=BC=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 59 ++++----- lib/pages/main_content_page.dart | 202 ++++++++++++----------------- lib/utils/theme/app_theme.dart | 2 - lib/utils/usage_stats_service.dart | 4 +- pubspec.yaml | 2 +- 5 files changed, 112 insertions(+), 157 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index db902a5..fab8546 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -44,7 +44,6 @@ Future _initUsageStats() async { class MyApp extends StatefulWidget { final AppProvider appProvider; - const MyApp({super.key, required this.appProvider}); @override @@ -52,52 +51,50 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State with WidgetsBindingObserver { + ThemeMode? _lastAppliedTheme; + @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); widget.appProvider.loadThemeMode(); widget.appProvider.addListener(_onThemeChanged); - _updateSystemUI(widget.appProvider.themeMode); - } - - @override - void dispose() { - widget.appProvider.removeListener(_onThemeChanged); - WidgetsBinding.instance.removeObserver(this); - super.dispose(); + // 延迟到首帧后确保生效 + WidgetsBinding.instance.addPostFrameCallback((_) => _applySystemUI()); } void _onThemeChanged() { - _updateSystemUI(widget.appProvider.themeMode); + final current = widget.appProvider.themeMode; + if (_lastAppliedTheme != current) { + _applySystemUI(); + } + } + + void _applySystemUI() { + final mode = widget.appProvider.themeMode; + final Brightness brightness; + switch (mode) { + case ThemeMode.light: brightness = Brightness.light; + case ThemeMode.dark: brightness = Brightness.dark; + case ThemeMode.system: brightness = PlatformDispatcher.instance.platformBrightness; + } + _lastAppliedTheme = mode; + final isDark = brightness == Brightness.dark; + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark, + systemNavigationBarColor: isDark ? Colors.black : Colors.white, + systemNavigationBarIconBrightness: isDark ? Brightness.light : Brightness.dark, + )); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { - _updateSystemUI(widget.appProvider.themeMode); + _applySystemUI(); } } - void _updateSystemUI(ThemeMode mode) { - final Brightness brightness; - switch (mode) { - case ThemeMode.light: - brightness = Brightness.light; - case ThemeMode.dark: - brightness = Brightness.dark; - case ThemeMode.system: - brightness = PlatformDispatcher.instance.platformBrightness; - } - final isDark = brightness == Brightness.dark; - SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark, - systemNavigationBarColor: isDark ? const Color(0xFF1A1A1A) : Colors.white, - systemNavigationBarIconBrightness: isDark ? Brightness.light : Brightness.dark, - )); - } - @override Widget build(BuildContext context) { final iconName = UserPrefs().appIconName; @@ -138,7 +135,6 @@ class _MyAppState extends State with WidgetsBindingObserver { class _AppIconWrapper extends StatefulWidget { final Widget child; final String iconName; - const _AppIconWrapper({required this.child, required this.iconName}); @override @@ -163,7 +159,6 @@ class _AppIconWrapperState extends State<_AppIconWrapper> { @Preview(name: "MookNote App Preview") Widget previewMyApp() { final appProvider = AppProvider(); - return MultiProvider( providers: [ ChangeNotifierProvider.value(value: appProvider), diff --git a/lib/pages/main_content_page.dart b/lib/pages/main_content_page.dart index 48b1119..72962e7 100644 --- a/lib/pages/main_content_page.dart +++ b/lib/pages/main_content_page.dart @@ -10,7 +10,7 @@ import 'note/note_tab_page.dart'; import 'search_page.dart'; import 'sync/webdav_sync_page.dart'; -/// 主内容页 - 观影/阅读/笔记标签页 +/// 主内容页 - 观影/阅读/笔记标签页(PageView 滑动切换) class MainContentPage extends StatefulWidget { const MainContentPage({super.key}); @@ -25,10 +25,20 @@ class _MainContentPageState extends State { bool _showBookTab = true; bool _showNoteTab = true; + late PageController _pageController; + bool _isTabTap = false; // 防止点击 Tab 和滑动互斥 + @override void initState() { super.initState(); _loadTabSettings(); + _pageController = PageController(initialPage: 0); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); } void _loadTabSettings() { @@ -118,32 +128,11 @@ class _MainContentPageState extends State { )), Text('云备份', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: bc.onSurface)), const SizedBox(height: 20), - _cloudCard( - icon: Icons.cloud_upload_outlined, - title: '上传数据', - desc: hasConfig ? '将本地数据同步到云端' : '请先配置 WebDAV 服务器', - onTap: hasConfig ? () { Navigator.pop(ctx); _performSync(context, SyncDirection.upload); } : null, - enabled: hasConfig, - colors: bc, - ), + _cloudCard(icon: Icons.cloud_upload_outlined, title: '上传数据', desc: hasConfig ? '将本地数据同步到云端' : '请先配置 WebDAV 服务器', enabled: hasConfig, onTap: hasConfig ? () { Navigator.pop(ctx); _performSync(context, SyncDirection.upload); } : null, colors: bc), const SizedBox(height: 12), - _cloudCard( - icon: Icons.cloud_download_outlined, - title: '下载数据', - desc: hasConfig ? '从云端恢复数据到本地' : '请先配置 WebDAV 服务器', - onTap: hasConfig ? () { Navigator.pop(ctx); _performSync(context, SyncDirection.download); } : null, - enabled: hasConfig, - colors: bc, - ), + _cloudCard(icon: Icons.cloud_download_outlined, title: '下载数据', desc: hasConfig ? '从云端恢复数据到本地' : '请先配置 WebDAV 服务器', enabled: hasConfig, onTap: hasConfig ? () { Navigator.pop(ctx); _performSync(context, SyncDirection.download); } : null, colors: bc), const SizedBox(height: 12), - _cloudCard( - icon: Icons.settings_outlined, - title: 'WebDAV 设置', - desc: '配置服务器地址与认证信息', - onTap: () { Navigator.pop(ctx); Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())); }, - enabled: true, - colors: bc, - ), + _cloudCard(icon: Icons.settings_outlined, title: 'WebDAV 设置', desc: '配置服务器地址与认证信息', enabled: true, onTap: () { Navigator.pop(ctx); Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())); }, colors: bc), ]), ), ); @@ -162,14 +151,7 @@ class _MainContentPageState extends State { border: Border.all(color: enabled ? colors.primary.withValues(alpha: 0.1) : colors.outlineVariant, width: 0.5), ), child: Row(children: [ - Container( - width: 48, height: 48, - decoration: BoxDecoration( - color: enabled ? colors.primary.withValues(alpha: 0.08) : colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - ), - child: Icon(icon, size: 24, color: enabled ? colors.primary : colors.onSurface.withValues(alpha: 0.18)), - ), + Container(width: 48, height: 48, decoration: BoxDecoration(color: enabled ? colors.primary.withValues(alpha: 0.08) : colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(12)), child: Icon(icon, size: 24, color: enabled ? colors.primary : colors.onSurface.withValues(alpha: 0.18))), const SizedBox(width: 16), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: enabled ? colors.onSurface : colors.onSurface.withValues(alpha: 0.25))), @@ -201,12 +183,7 @@ class _MainContentPageState extends State { await provider.loadNotes(); } if (context.mounted) { - _showResultDialog(context, - title: result.success ? '同步成功' : '同步失败', - message: result.message.isNotEmpty ? result.message : (result.success ? '同步成功' : '同步失败'), - isSuccess: result.success, - details: {'uploaded': result.uploadedFiles + result.uploadedImages, 'downloaded': result.downloadedFiles + result.downloadedImages}, - ); + _showResultDialog(context, title: result.success ? '同步成功' : '同步失败', message: result.message.isNotEmpty ? result.message : (result.success ? '同步成功' : '同步失败'), isSuccess: result.success, details: {'uploaded': result.uploadedFiles + result.uploadedImages, 'downloaded': result.downloadedFiles + result.downloadedImages}); } } @@ -218,31 +195,18 @@ class _MainContentPageState extends State { backgroundColor: colors.surface, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), title: Row(children: [ - Container( - width: 40, height: 40, - decoration: BoxDecoration(color: isSuccess ? const Color(0xFFE8F5E9) : const Color(0xFFFFEBEE), borderRadius: BorderRadius.circular(10)), - child: Icon(isSuccess ? Icons.check_circle : Icons.error, color: isSuccess ? const Color(0xFF4CAF50) : const Color(0xFFE57373), size: 24), - ), + Container(width: 40, height: 40, decoration: BoxDecoration(color: isSuccess ? const Color(0xFFE8F5E9) : const Color(0xFFFFEBEE), borderRadius: BorderRadius.circular(10)), child: Icon(isSuccess ? Icons.check_circle : Icons.error, color: isSuccess ? const Color(0xFF4CAF50) : const Color(0xFFE57373), size: 24)), const SizedBox(width: 12), Text(title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), ]), content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(message, style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)), - if (details != null) ...[ - const SizedBox(height: 16), - Container(padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(8)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (details['uploaded'] != null) _detailRow('上传文件', '${details['uploaded']} 个', colors), - if (details['downloaded'] != null) _detailRow('下载文件', '${details['downloaded']} 个', colors), - ])), - ], + if (details != null) ...[const SizedBox(height: 16), Container(padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(8)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (details['uploaded'] != null) _detailRow('上传文件', '${details['uploaded']} 个', colors), + if (details['downloaded'] != null) _detailRow('下载文件', '${details['downloaded']} 个', colors), + ]))], ]), - actions: [ - ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom(backgroundColor: colors.primary, foregroundColor: colors.onPrimary, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12)), - child: const Text('确定'), - ), - ], + actions: [ElevatedButton(onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom(backgroundColor: colors.primary, foregroundColor: colors.onPrimary, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12)), child: const Text('确定'))], actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), ); @@ -265,6 +229,10 @@ class _MainContentPageState extends State { } } + // ═══════════════════════════════════════════════════════════════════ + // Tab 栏 + 指示条(跟随 PageView 滑动) + // ═══════════════════════════════════════════════════════════════════ + Widget _buildTabBar(BuildContext context) { return Consumer( builder: (context, provider, child) { @@ -286,47 +254,47 @@ class _MainContentPageState extends State { return Expanded( child: GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => provider.setMainTabIndex(tab.originalIndex), + onTap: () { + _isTabTap = true; + _pageController.animateToPage(idx, duration: const Duration(milliseconds: 350), curve: Curves.easeInOut); + provider.setMainTabIndex(tab.originalIndex); + Future.delayed(const Duration(milliseconds: 400), () => _isTabTap = false); + }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - _tabIcon(tab.label), - size: 18, - color: selected ? colors.primary : colors.onSurface.withValues(alpha: 0.3), - ), - const SizedBox(width: 5), - Text(tab.label, textAlign: TextAlign.center, style: TextStyle( - fontSize: 15, - fontWeight: selected ? FontWeight.w700 : FontWeight.w500, - color: selected ? colors.primary : colors.onSurface.withValues(alpha: 0.3), - )), - ], - ), + child: Row(mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ + Icon(_tabIcon(tab.label), size: 18, color: selected ? colors.primary : colors.onSurface.withValues(alpha: 0.3)), + const SizedBox(width: 5), + Text(tab.label, textAlign: TextAlign.center, style: TextStyle(fontSize: 15, fontWeight: selected ? FontWeight.w700 : FontWeight.w500, color: selected ? colors.primary : colors.onSurface.withValues(alpha: 0.3))), + ]), ), ), ); }).toList(), ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: LayoutBuilder( - builder: (context, constraints) { - final tabWidth = tabs.isNotEmpty ? constraints.maxWidth / tabs.length : 0.0; - return SizedBox(height: 2.5, child: Stack(children: [ - AnimatedPositioned( - duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, - left: safeIndex * tabWidth, top: 0, - width: tabWidth, - child: Container(height: 2.5, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(2))), - ), - ])); - }, - ), + // 指示条 — 跟随 PageView 滑动 + AnimatedBuilder( + animation: _pageController, + builder: (context, _) { + final page = _pageController.hasClients ? _pageController.page ?? 0.0 : safeIndex.toDouble(); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: LayoutBuilder( + builder: (context, constraints) { + final tabWidth = tabs.isNotEmpty ? constraints.maxWidth / tabs.length : 0.0; + return SizedBox(height: 2.5, child: Stack(children: [ + Positioned( + left: page * tabWidth, + top: 0, + width: tabWidth, + child: Container(height: 2.5, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(2))), + ), + ])); + }, + ), + ); + }, ), ]), ); @@ -334,23 +302,34 @@ class _MainContentPageState extends State { ); } + // ═══════════════════════════════════════════════════════════════════ + // PageView 内容区 + // ═══════════════════════════════════════════════════════════════════ + Widget _buildTabContent() { return Consumer( builder: (context, provider, child) { final tabs = _enabledTabs; - _TabItem? currentTab; - for (final tab in tabs) { if (tab.originalIndex == provider.mainTabIndex) { currentTab = tab; break; } } - if (currentTab == null && tabs.isNotEmpty) { - currentTab = tabs.first; - WidgetsBinding.instance.addPostFrameCallback((_) => provider.setMainTabIndex(currentTab!.originalIndex)); - } - if (currentTab == null) return const Center(child: Text('请至少启用一个标签页')); - switch (currentTab.originalIndex) { - case 0: return const MovieTabPage(); - case 1: return const BookTabPage(); - case 2: return const NoteTabPage(); - default: return const MovieTabPage(); + final safeIndex = _mapToEnabledTabIndex(provider.mainTabIndex).clamp(0, tabs.length - 1); + + // 同步 provider → PageView(点击 Tab 触发) + if (_isTabTap && _pageController.hasClients) { + _pageController.animateToPage(safeIndex, duration: const Duration(milliseconds: 350), curve: Curves.easeInOut); } + + return PageView( + controller: _pageController, + onPageChanged: (index) { + if (!_isTabTap && index < tabs.length) { + provider.setMainTabIndex(tabs[index].originalIndex); + } + }, + children: [ + if (_showMovieTab) const MovieTabPage(), + if (_showBookTab) const BookTabPage(), + if (_showNoteTab) const NoteTabPage(), + ], + ); }, ); } @@ -363,23 +342,6 @@ class _MainContentPageState extends State { default: return Icons.circle; } } - - void _showAddDialog(BuildContext context, AppProvider provider) { - final colors = Theme.of(context).colorScheme; - showModalBottomSheet( - context: context, backgroundColor: colors.surface, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), - builder: (_) => SafeArea( - child: Wrap(children: [ - ListTile(leading: Icon(Icons.movie, color: colors.onSurface), title: const Text('添加观影'), onTap: () { Navigator.pop(context); Navigator.pushNamed(context, '/movie-form', arguments: {'initialStatus': ['watched', 'watching', 'want_to_watch'][provider.movieStatusIndex]}); }), - Divider(height: 0.5, indent: 56, color: colors.outlineVariant), - ListTile(leading: Icon(Icons.menu_book, color: colors.onSurface), title: const Text('添加阅读'), onTap: () { Navigator.pop(context); Navigator.pushNamed(context, '/book-form', arguments: {'initialStatus': ['read', 'reading', 'want_to_read'][provider.bookStatusIndex]}); }), - Divider(height: 0.5, indent: 56, color: colors.outlineVariant), - ListTile(leading: Icon(Icons.note, color: colors.onSurface), title: const Text('添加笔记'), onTap: () { Navigator.pop(context); Navigator.pushNamed(context, '/note-form'); }), - ]), - ), - ); - } } class _TabItem { diff --git a/lib/utils/theme/app_theme.dart b/lib/utils/theme/app_theme.dart index c740a88..a083d15 100644 --- a/lib/utils/theme/app_theme.dart +++ b/lib/utils/theme/app_theme.dart @@ -64,7 +64,6 @@ class AppTheme { elevation: 0, centerTitle: false, titleSpacing: 24, - systemOverlayStyle: SystemUiOverlayStyle.dark, titleTextStyle: TextStyle( fontFamily: _fontFamily, fontSize: 18, @@ -278,7 +277,6 @@ class AppTheme { elevation: 0, centerTitle: false, titleSpacing: 24, - systemOverlayStyle: SystemUiOverlayStyle.light, titleTextStyle: TextStyle( fontFamily: _fontFamily, fontSize: 18, diff --git a/lib/utils/usage_stats_service.dart b/lib/utils/usage_stats_service.dart index 24b0845..b2837ff 100644 --- a/lib/utils/usage_stats_service.dart +++ b/lib/utils/usage_stats_service.dart @@ -17,8 +17,8 @@ class UsageStatsService with WidgetsBindingObserver { final UserPrefs _prefs = UserPrefs(); /// 统计服务器地址,发布前替换为实际地址,置空则禁用 - // static String serverUrl = 'http://api.mooknote.iletter.top/'; - static String serverUrl = 'http://192.168.31.48:27050/'; + static String serverUrl = 'http://api.mooknote.iletter.top/'; + // static String serverUrl = 'http://192.168.31.48:27050/'; Timer? _heartbeatTimer; bool _started = false; diff --git a/pubspec.yaml b/pubspec.yaml index 9944231..1f79a1e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: mooknote description: "app for tracking movies, books, and notes" publish_to: 'none' -version: 0.1.8 +version: 0.1.9 environment: sdk: ^3.5.0