新增标签自主控制功能

This commit is contained in:
DelLevin-Home
2026-03-12 16:13:13 +08:00
parent 0f221456cb
commit 19e530577b
10 changed files with 543 additions and 76 deletions

View File

@@ -121,3 +121,7 @@ flutter run
- 目前图片同步是基于文件存在性判断,不是基于修改时间
- 下载新数据库后,应用会自动重新加载数据(调用 Provider 的 load 方法)
- 首次同步会创建远程目录结构
## 开源协议
本项目采用 [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html) 开源协议。

View File

@@ -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),
);
}

View File

@@ -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<MainContentPage> createState() => _MainContentPageState();
}
class _MainContentPageState extends State<MainContentPage> {
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<AppProvider>(
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<AppProvider>(
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);
}

View File

@@ -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<ProfilePage> {
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<void> _loadVersionInfo() async {
final packageInfo = await PackageInfo.fromPlatform();
setState(() {
_version = packageInfo.version;
});
}
/// 加载用户数据
@@ -100,10 +111,10 @@ class _ProfilePageState extends State<ProfilePage> {
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<MainContentSettingsPage> createState() => _MainContentSettingsPageState();
}
class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
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<void> _toggleMovieTab(bool value) async {
if (!value && _enabledTabCount <= 1) {
_showToast('至少保留一个标签页');
return;
}
await _userPrefs.setShowMovieTab(value);
setState(() => _showMovieTab = value);
}
/// 切换阅读标签显示
Future<void> _toggleBookTab(bool value) async {
if (!value && _enabledTabCount <= 1) {
_showToast('至少保留一个标签页');
return;
}
await _userPrefs.setShowBookTab(value);
setState(() => _showBookTab = value);
}
/// 切换笔记标签显示
Future<void> _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<bool> 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;

View File

@@ -45,4 +45,18 @@ class UserPrefs {
/// 是否首次启动
bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true;
Future<bool> setFirstLaunch(bool value) => prefs.setBool('isFirstLaunch', value);
// ========== 主界面显示设置 ==========
/// 是否显示观影标签
bool get showMovieTab => prefs.getBool('showMovieTab') ?? true;
Future<bool> setShowMovieTab(bool value) => prefs.setBool('showMovieTab', value);
/// 是否显示阅读标签
bool get showBookTab => prefs.getBool('showBookTab') ?? true;
Future<bool> setShowBookTab(bool value) => prefs.setBool('showBookTab', value);
/// 是否显示笔记标签
bool get showNoteTab => prefs.getBool('showNoteTab') ?? true;
Future<bool> setShowNoteTab(bool value) => prefs.setBool('showNoteTab', value);
}

View File

@@ -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(

View File

@@ -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<CustomDrawer> createState() => _CustomDrawerState();
}
class _CustomDrawerState extends State<CustomDrawer> {
String _version = '0.1.5';
@override
void initState() {
super.initState();
_loadVersionInfo();
}
/// 加载版本信息
Future<void> _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),
),

View File

@@ -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(

View File

@@ -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:

View File

@@ -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: