diff --git a/README.md b/README.md index 62e32ba..831e6af 100644 --- a/README.md +++ b/README.md @@ -121,3 +121,7 @@ flutter run - 目前图片同步是基于文件存在性判断,不是基于修改时间 - 下载新数据库后,应用会自动重新加载数据(调用 Provider 的 load 方法) - 首次同步会创建远程目录结构 + +## 开源协议 + +本项目采用 [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html) 开源协议。 diff --git a/lib/main.dart b/lib/main.dart index 40175a8..66a7d9d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'utils/user_prefs.dart'; import 'utils/sync/webdav_service.dart'; import 'utils/sync/auto_backup_service.dart'; import 'providers/app_provider.dart'; +import 'package:flutter/widget_previews.dart'; void main() async { // 确保 Flutter 绑定初始化完成 @@ -67,3 +68,16 @@ class MyApp extends StatelessWidget { ); } } + +/// 用于预览 MyApp 的 Widget +@Preview(name: "MookNote App Preview") // 添加 @Preview 注解 +Widget previewMyApp() { + final appProvider = AppProvider(); + + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: appProvider), + ], + child: MyApp(appProvider: appProvider), + ); +} \ No newline at end of file diff --git a/lib/pages/main_content_page.dart b/lib/pages/main_content_page.dart index f0ec501..1b5cc5b 100644 --- a/lib/pages/main_content_page.dart +++ b/lib/pages/main_content_page.dart @@ -1,15 +1,67 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; +import '../utils/user_prefs.dart'; import 'movies/movie_tab_page.dart'; import 'book/book_tab_page.dart'; import 'note/note_tab_page.dart'; import 'search_page.dart'; /// 主内容页 - 观影/阅读/笔记标签页 -class MainContentPage extends StatelessWidget { +class MainContentPage extends StatefulWidget { const MainContentPage({super.key}); + @override + State createState() => _MainContentPageState(); +} + +class _MainContentPageState extends State { + final UserPrefs _userPrefs = UserPrefs(); + + bool _showMovieTab = true; + bool _showBookTab = true; + bool _showNoteTab = true; + + @override + void initState() { + super.initState(); + _loadTabSettings(); + } + + /// 加载标签显示设置 + void _loadTabSettings() { + setState(() { + _showMovieTab = _userPrefs.showMovieTab; + _showBookTab = _userPrefs.showBookTab; + _showNoteTab = _userPrefs.showNoteTab; + }); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // 当页面重新获得焦点时刷新设置 + _loadTabSettings(); + } + + /// 获取启用的标签列表 + List<_TabItem> get _enabledTabs { + final tabs = <_TabItem>[]; + if (_showMovieTab) tabs.add(_TabItem('观影', 0)); + if (_showBookTab) tabs.add(_TabItem('阅读', 1)); + if (_showNoteTab) tabs.add(_TabItem('笔记', 2)); + return tabs; + } + + /// 将原始索引映射到启用标签的索引 + int _mapToEnabledTabIndex(int originalIndex) { + final tabs = _enabledTabs; + for (int i = 0; i < tabs.length; i++) { + if (tabs[i].originalIndex == originalIndex) return i; + } + return 0; + } + @override Widget build(BuildContext context) { return Column( @@ -71,6 +123,11 @@ class MainContentPage extends StatelessWidget { Widget _buildTabBar(BuildContext context) { return Consumer( builder: (context, provider, child) { + final tabs = _enabledTabs; + // 如果当前选中的标签被禁用了,切换到第一个启用的标签 + final currentEnabledIndex = _mapToEnabledTabIndex(provider.mainTabIndex); + final safeIndex = currentEnabledIndex < tabs.length ? currentEnabledIndex : 0; + return Container( decoration: const BoxDecoration( color: Colors.white, @@ -79,29 +136,17 @@ class MainContentPage extends StatelessWidget { ), ), child: Row( - children: [ - _buildTabItem( + children: tabs.asMap().entries.map((entry) { + final index = entry.key; + final tab = entry.value; + return _buildTabItem( context, - '观影', - 0, - provider.mainTabIndex, - () => provider.setMainTabIndex(0), - ), - _buildTabItem( - context, - '阅读', - 1, - provider.mainTabIndex, - () => provider.setMainTabIndex(1), - ), - _buildTabItem( - context, - '笔记', - 2, - provider.mainTabIndex, - () => provider.setMainTabIndex(2), - ), - ], + tab.label, + index, + safeIndex, + () => provider.setMainTabIndex(tab.originalIndex), + ); + }).toList(), ), ); }, @@ -154,7 +199,29 @@ class MainContentPage extends StatelessWidget { Widget _buildTabContent() { return Consumer( builder: (context, provider, child) { - switch (provider.mainTabIndex) { + 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; + // 更新 provider 的索引 + 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: @@ -233,3 +300,11 @@ class MainContentPage extends StatelessWidget { ); } } + +/// 标签项数据类 +class _TabItem { + final String label; + final int originalIndex; + + _TabItem(this.label, this.originalIndex); +} diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart index fba21d8..7beb1d1 100644 --- a/lib/pages/profile_page.dart +++ b/lib/pages/profile_page.dart @@ -4,6 +4,7 @@ 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:package_info_plus/package_info_plus.dart'; // import 'package:url_launcher/url_launcher.dart'; // 改为应用内打开 import 'package:webview_flutter/webview_flutter.dart'; import '../providers/app_provider.dart'; @@ -25,17 +26,27 @@ class ProfilePage extends StatefulWidget { class _ProfilePageState extends State { final ImagePicker _picker = ImagePicker(); final UserPrefs _userPrefs = UserPrefs(); - + // 用户数据 String _nickname = 'Mook'; String _motto = '好运不会眷顾一无所有之人。'; String? _avatarPath; + String _version = '0.1.5'; bool _isLoading = true; - + @override void initState() { super.initState(); _loadUserData(); + _loadVersionInfo(); + } + + /// 加载版本信息 + Future _loadVersionInfo() async { + final packageInfo = await PackageInfo.fromPlatform(); + setState(() { + _version = packageInfo.version; + }); } /// 加载用户数据 @@ -100,10 +111,10 @@ class _ProfilePageState extends State { const SizedBox(height: 48), // 版本信息 - const Center( + Center( child: Text( - 'MookNote v0.1.5', - style: TextStyle( + 'MookNote v$_version', + style: const TextStyle( fontSize: 12, color: Color(0xFF999999), ), @@ -617,7 +628,25 @@ class SettingsPage extends StatelessWidget { ), body: ListView( children: [ + // 主界面功能显示入口 + _buildSectionHeader('主界面显示'), + _buildNavigationItem( + icon: Icons.view_list_outlined, + title: '主界面功能显示', + subtitle: '控制观影、阅读、笔记的显示', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const MainContentSettingsPage(), + ), + ); + }, + ), + const Divider(height: 0.5, indent: 24, endIndent: 24), + // 使用说明 + _buildSectionHeader('帮助'), _buildLinkItem( context: context, icon: Icons.help_outline, @@ -626,7 +655,7 @@ class SettingsPage extends StatelessWidget { url: 'https://mooknote.iletter.top/#/guide', ), const Divider(height: 0.5, indent: 24, endIndent: 24), - + // 关于作者 _buildLinkItem( context: context, @@ -635,13 +664,74 @@ class SettingsPage extends StatelessWidget { subtitle: '了解更多信息', url: 'https://www.iletter.top/', ), - + const Divider(height: 0.5, indent: 24, endIndent: 24), ], ), ); } + /// 构建区块标题 + Widget _buildSectionHeader(String title) { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Text( + title.toUpperCase(), + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: Color(0xFF999999), + letterSpacing: 1, + ), + ), + ); + } + + /// 构建导航项 + Widget _buildNavigationItem({ + required IconData icon, + required String title, + required String subtitle, + required VoidCallback onTap, + }) { + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: const Color(0xFF666666), + size: 20, + ), + ), + title: Text( + title, + style: const TextStyle( + fontSize: 15, + color: Color(0xFF1A1A1A), + ), + ), + subtitle: Text( + subtitle, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF999999), + ), + ), + trailing: const Icon( + Icons.chevron_right, + color: Color(0xFFCCCCCC), + size: 20, + ), + onTap: onTap, + ); + } + /// 构建链接项 Widget _buildLinkItem({ required BuildContext context, @@ -699,6 +789,183 @@ class SettingsPage extends StatelessWidget { } } +/// 主界面功能显示设置页面 +class MainContentSettingsPage extends StatefulWidget { + const MainContentSettingsPage({super.key}); + + @override + State createState() => _MainContentSettingsPageState(); +} + +class _MainContentSettingsPageState extends State { + final UserPrefs _userPrefs = UserPrefs(); + + bool _showMovieTab = true; + bool _showBookTab = true; + bool _showNoteTab = true; + + @override + void initState() { + super.initState(); + _loadSettings(); + } + + /// 加载设置 + void _loadSettings() { + setState(() { + _showMovieTab = _userPrefs.showMovieTab; + _showBookTab = _userPrefs.showBookTab; + _showNoteTab = _userPrefs.showNoteTab; + }); + } + + /// 获取已启用的标签数量 + int get _enabledTabCount { + int count = 0; + if (_showMovieTab) count++; + if (_showBookTab) count++; + if (_showNoteTab) count++; + return count; + } + + /// 切换观影标签显示 + Future _toggleMovieTab(bool value) async { + if (!value && _enabledTabCount <= 1) { + _showToast('至少保留一个标签页'); + return; + } + await _userPrefs.setShowMovieTab(value); + setState(() => _showMovieTab = value); + } + + /// 切换阅读标签显示 + Future _toggleBookTab(bool value) async { + if (!value && _enabledTabCount <= 1) { + _showToast('至少保留一个标签页'); + return; + } + await _userPrefs.setShowBookTab(value); + setState(() => _showBookTab = value); + } + + /// 切换笔记标签显示 + Future _toggleNoteTab(bool value) async { + if (!value && _enabledTabCount <= 1) { + _showToast('至少保留一个标签页'); + return; + } + await _userPrefs.setShowNoteTab(value); + setState(() => _showNoteTab = value); + } + + /// 显示提示 + void _showToast(String message) { + ToastUtil.show(context, message); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + title: const Text('主界面功能显示'), + ), + body: ListView( + children: [ + // 说明文字 + Container( + padding: const EdgeInsets.all(24), + child: const Text( + '选择要在主界面显示的功能模块,至少保留一个。', + style: TextStyle( + fontSize: 14, + color: Color(0xFF666666), + ), + ), + ), + const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), + // 观影开关 + _buildSwitchItem( + icon: Icons.movie_outlined, + title: '观影', + subtitle: '记录和管理观影记录', + value: _showMovieTab, + onChanged: _toggleMovieTab, + ), + const Divider(height: 0.5, indent: 24, endIndent: 24), + // 阅读开关 + _buildSwitchItem( + icon: Icons.menu_book_outlined, + title: '阅读', + subtitle: '记录和管理阅读记录', + value: _showBookTab, + onChanged: _toggleBookTab, + ), + const Divider(height: 0.5, indent: 24, endIndent: 24), + // 笔记开关 + _buildSwitchItem( + icon: Icons.note_outlined, + title: '笔记', + subtitle: '记录和管理笔记', + value: _showNoteTab, + onChanged: _toggleNoteTab, + ), + const Divider(height: 0.5, indent: 24, endIndent: 24), + ], + ), + ); + } + + /// 构建开关项 + Widget _buildSwitchItem({ + required IconData icon, + required String title, + required String subtitle, + required bool value, + required ValueChanged onChanged, + }) { + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + leading: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: const Color(0xFF666666), + size: 24, + ), + ), + title: Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF1A1A1A), + ), + ), + subtitle: Text( + subtitle, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF999999), + ), + ), + trailing: Switch( + value: value, + onChanged: onChanged, + activeColor: const Color(0xFF1A1A1A), + activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), + inactiveThumbColor: Colors.white, + inactiveTrackColor: const Color(0xFFE5E5E5), + ), + ); + } +} + /// WebView 页面 class WebViewPage extends StatefulWidget { final String url; diff --git a/lib/utils/user_prefs.dart b/lib/utils/user_prefs.dart index 9cfbd74..cb00813 100644 --- a/lib/utils/user_prefs.dart +++ b/lib/utils/user_prefs.dart @@ -45,4 +45,18 @@ class UserPrefs { /// 是否首次启动 bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true; Future setFirstLaunch(bool value) => prefs.setBool('isFirstLaunch', value); + + // ========== 主界面显示设置 ========== + + /// 是否显示观影标签 + bool get showMovieTab => prefs.getBool('showMovieTab') ?? true; + Future setShowMovieTab(bool value) => prefs.setBool('showMovieTab', value); + + /// 是否显示阅读标签 + bool get showBookTab => prefs.getBool('showBookTab') ?? true; + Future setShowBookTab(bool value) => prefs.setBool('showBookTab', value); + + /// 是否显示笔记标签 + bool get showNoteTab => prefs.getBool('showNoteTab') ?? true; + Future setShowNoteTab(bool value) => prefs.setBool('showNoteTab', value); } diff --git a/lib/widgets/book_list_item.dart b/lib/widgets/book_list_item.dart index 1e5dd4a..c654888 100644 --- a/lib/widgets/book_list_item.dart +++ b/lib/widgets/book_list_item.dart @@ -42,26 +42,9 @@ class BookListItem extends StatelessWidget { const SizedBox(height: 4), - // 评分 + // 评分 - 5星显示 if (book.rating != null) - Row( - children: [ - const Icon( - Icons.star, - size: 12, - color: Color(0xFFFFB800), - ), - const SizedBox(width: 2), - Text( - book.rating!.toStringAsFixed(1), - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Color(0xFF666666), - ), - ), - ], - ) + _buildStarRating(book.rating!) else const SizedBox(height: 16), ], @@ -99,6 +82,50 @@ class BookListItem extends StatelessWidget { ); } + /// 构建5星评分显示(评分范围1-10,每星2分) + Widget _buildStarRating(double rating) { + // 将10分制转换为5星制 + final starValue = rating / 2; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + // 5个星星 + ...List.generate(5, (index) { + final starIndex = index + 1; + IconData iconData; + + if (starValue >= starIndex) { + // 满星 + iconData = Icons.star; + } else if (starValue >= starIndex - 0.5) { + // 半星 + iconData = Icons.star_half; + } else { + // 空星 + iconData = Icons.star_border; + } + + return Icon( + iconData, + size: 12, + color: const Color(0xFFFFB800), + ); + }), + const SizedBox(width: 4), + // 评分数字 + Text( + rating.toStringAsFixed(1), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Color(0xFF666666), + ), + ), + ], + ); + } + /// 显示删除确认对话框 void _showDeleteDialog(BuildContext context) { showDialog( diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index 372cdb1..a04faee 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -2,14 +2,36 @@ import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; 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 '../utils/toast_util.dart'; import '../models/data_models.dart'; /// 自定义左侧弹出菜单 - 极简主义设计 -class CustomDrawer extends StatelessWidget { - CustomDrawer({super.key}); +class CustomDrawer extends StatefulWidget { + const CustomDrawer({super.key}); + + @override + State createState() => _CustomDrawerState(); +} + +class _CustomDrawerState extends State { + String _version = '0.1.5'; + + @override + void initState() { + super.initState(); + _loadVersionInfo(); + } + + /// 加载版本信息 + Future _loadVersionInfo() async { + final packageInfo = await PackageInfo.fromPlatform(); + setState(() { + _version = packageInfo.version; + }); + } @override Widget build(BuildContext context) { @@ -43,9 +65,9 @@ class CustomDrawer extends StatelessWidget { // 底部版本信息 Container( padding: const EdgeInsets.all(24), - child: const Text( - 'MookNote v0.1.5', - style: TextStyle( + child: Text( + 'MookNote v$_version', + style: const TextStyle( fontSize: 12, color: Color(0xFF999999), ), diff --git a/lib/widgets/movie_list_item.dart b/lib/widgets/movie_list_item.dart index 70c2f7b..ed66687 100644 --- a/lib/widgets/movie_list_item.dart +++ b/lib/widgets/movie_list_item.dart @@ -42,26 +42,9 @@ class MovieListItem extends StatelessWidget { const SizedBox(height: 4), - // 评分 + // 评分 - 5星显示 if (movie.rating != null) - Row( - children: [ - const Icon( - Icons.star, - size: 12, - color: Color(0xFFFFB800), - ), - const SizedBox(width: 2), - Text( - movie.rating!.toStringAsFixed(1), - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Color(0xFF666666), - ), - ), - ], - ) + _buildStarRating(movie.rating!) else const SizedBox(height: 16), ], @@ -99,6 +82,50 @@ class MovieListItem extends StatelessWidget { ); } + /// 构建5星评分显示(评分范围1-10,每星2分) + Widget _buildStarRating(double rating) { + // 将10分制转换为5星制 + final starValue = rating / 2; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + // 5个星星 + ...List.generate(5, (index) { + final starIndex = index + 1; + IconData iconData; + + if (starValue >= starIndex) { + // 满星 + iconData = Icons.star; + } else if (starValue >= starIndex - 0.5) { + // 半星 + iconData = Icons.star_half; + } else { + // 空星 + iconData = Icons.star_border; + } + + return Icon( + iconData, + size: 12, + color: const Color(0xFFFFB800), + ); + }), + const SizedBox(width: 4), + // 评分数字 + Text( + rating.toStringAsFixed(1), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Color(0xFF666666), + ), + ), + ], + ); + } + /// 显示删除确认对话框 void _showDeleteDialog(BuildContext context) { showDialog( diff --git a/pubspec.lock b/pubspec.lock index be22f9b..9950ab5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -464,6 +464,22 @@ packages: url: "https://pub.dev" source: hosted version: "9.3.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://pub.dev" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d749742..ca3aa25 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: mooknote -description: "A Flutter application for tracking movies, books, and notes" +description: "app for tracking movies, books, and notes" publish_to: 'none' -version: 0.1.5+1 +version: 0.1.6+1 environment: sdk: ^3.5.0 @@ -27,6 +27,7 @@ dependencies: http: ^1.2.0 url_launcher: ^6.2.5 webview_flutter: ^4.8.0 + package_info_plus: ^8.0.0 dev_dependencies: flutter_test: