generated from dellevin/template
新增标签自主控制功能
This commit is contained in:
@@ -121,3 +121,7 @@ flutter run
|
|||||||
- 目前图片同步是基于文件存在性判断,不是基于修改时间
|
- 目前图片同步是基于文件存在性判断,不是基于修改时间
|
||||||
- 下载新数据库后,应用会自动重新加载数据(调用 Provider 的 load 方法)
|
- 下载新数据库后,应用会自动重新加载数据(调用 Provider 的 load 方法)
|
||||||
- 首次同步会创建远程目录结构
|
- 首次同步会创建远程目录结构
|
||||||
|
|
||||||
|
## 开源协议
|
||||||
|
|
||||||
|
本项目采用 [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html) 开源协议。
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'utils/user_prefs.dart';
|
|||||||
import 'utils/sync/webdav_service.dart';
|
import 'utils/sync/webdav_service.dart';
|
||||||
import 'utils/sync/auto_backup_service.dart';
|
import 'utils/sync/auto_backup_service.dart';
|
||||||
import 'providers/app_provider.dart';
|
import 'providers/app_provider.dart';
|
||||||
|
import 'package:flutter/widget_previews.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
// 确保 Flutter 绑定初始化完成
|
// 确保 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,15 +1,67 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../providers/app_provider.dart';
|
import '../providers/app_provider.dart';
|
||||||
|
import '../utils/user_prefs.dart';
|
||||||
import 'movies/movie_tab_page.dart';
|
import 'movies/movie_tab_page.dart';
|
||||||
import 'book/book_tab_page.dart';
|
import 'book/book_tab_page.dart';
|
||||||
import 'note/note_tab_page.dart';
|
import 'note/note_tab_page.dart';
|
||||||
import 'search_page.dart';
|
import 'search_page.dart';
|
||||||
|
|
||||||
/// 主内容页 - 观影/阅读/笔记标签页
|
/// 主内容页 - 观影/阅读/笔记标签页
|
||||||
class MainContentPage extends StatelessWidget {
|
class MainContentPage extends StatefulWidget {
|
||||||
const MainContentPage({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
@@ -71,6 +123,11 @@ class MainContentPage extends StatelessWidget {
|
|||||||
Widget _buildTabBar(BuildContext context) {
|
Widget _buildTabBar(BuildContext context) {
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
|
final tabs = _enabledTabs;
|
||||||
|
// 如果当前选中的标签被禁用了,切换到第一个启用的标签
|
||||||
|
final currentEnabledIndex = _mapToEnabledTabIndex(provider.mainTabIndex);
|
||||||
|
final safeIndex = currentEnabledIndex < tabs.length ? currentEnabledIndex : 0;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -79,29 +136,17 @@ class MainContentPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: tabs.asMap().entries.map((entry) {
|
||||||
_buildTabItem(
|
final index = entry.key;
|
||||||
|
final tab = entry.value;
|
||||||
|
return _buildTabItem(
|
||||||
context,
|
context,
|
||||||
'观影',
|
tab.label,
|
||||||
0,
|
index,
|
||||||
provider.mainTabIndex,
|
safeIndex,
|
||||||
() => provider.setMainTabIndex(0),
|
() => provider.setMainTabIndex(tab.originalIndex),
|
||||||
),
|
);
|
||||||
_buildTabItem(
|
}).toList(),
|
||||||
context,
|
|
||||||
'阅读',
|
|
||||||
1,
|
|
||||||
provider.mainTabIndex,
|
|
||||||
() => provider.setMainTabIndex(1),
|
|
||||||
),
|
|
||||||
_buildTabItem(
|
|
||||||
context,
|
|
||||||
'笔记',
|
|
||||||
2,
|
|
||||||
provider.mainTabIndex,
|
|
||||||
() => provider.setMainTabIndex(2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -154,7 +199,29 @@ class MainContentPage extends StatelessWidget {
|
|||||||
Widget _buildTabContent() {
|
Widget _buildTabContent() {
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
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:
|
case 0:
|
||||||
return const MovieTabPage();
|
return const MovieTabPage();
|
||||||
case 1:
|
case 1:
|
||||||
@@ -233,3 +300,11 @@ class MainContentPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 标签项数据类
|
||||||
|
class _TabItem {
|
||||||
|
final String label;
|
||||||
|
final int originalIndex;
|
||||||
|
|
||||||
|
_TabItem(this.label, this.originalIndex);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
// import 'package:url_launcher/url_launcher.dart'; // 改为应用内打开
|
// import 'package:url_launcher/url_launcher.dart'; // 改为应用内打开
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
import '../providers/app_provider.dart';
|
import '../providers/app_provider.dart';
|
||||||
@@ -25,17 +26,27 @@ class ProfilePage extends StatefulWidget {
|
|||||||
class _ProfilePageState extends State<ProfilePage> {
|
class _ProfilePageState extends State<ProfilePage> {
|
||||||
final ImagePicker _picker = ImagePicker();
|
final ImagePicker _picker = ImagePicker();
|
||||||
final UserPrefs _userPrefs = UserPrefs();
|
final UserPrefs _userPrefs = UserPrefs();
|
||||||
|
|
||||||
// 用户数据
|
// 用户数据
|
||||||
String _nickname = 'Mook';
|
String _nickname = 'Mook';
|
||||||
String _motto = '好运不会眷顾一无所有之人。';
|
String _motto = '好运不会眷顾一无所有之人。';
|
||||||
String? _avatarPath;
|
String? _avatarPath;
|
||||||
|
String _version = '0.1.5';
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadUserData();
|
_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 SizedBox(height: 48),
|
||||||
|
|
||||||
// 版本信息
|
// 版本信息
|
||||||
const Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'MookNote v0.1.5',
|
'MookNote v$_version',
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Color(0xFF999999),
|
color: Color(0xFF999999),
|
||||||
),
|
),
|
||||||
@@ -617,7 +628,25 @@ class SettingsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
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(
|
_buildLinkItem(
|
||||||
context: context,
|
context: context,
|
||||||
icon: Icons.help_outline,
|
icon: Icons.help_outline,
|
||||||
@@ -626,7 +655,7 @@ class SettingsPage extends StatelessWidget {
|
|||||||
url: 'https://mooknote.iletter.top/#/guide',
|
url: 'https://mooknote.iletter.top/#/guide',
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||||
|
|
||||||
// 关于作者
|
// 关于作者
|
||||||
_buildLinkItem(
|
_buildLinkItem(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -635,13 +664,74 @@ class SettingsPage extends StatelessWidget {
|
|||||||
subtitle: '了解更多信息',
|
subtitle: '了解更多信息',
|
||||||
url: 'https://www.iletter.top/',
|
url: 'https://www.iletter.top/',
|
||||||
),
|
),
|
||||||
|
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
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({
|
Widget _buildLinkItem({
|
||||||
required BuildContext context,
|
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 页面
|
/// WebView 页面
|
||||||
class WebViewPage extends StatefulWidget {
|
class WebViewPage extends StatefulWidget {
|
||||||
final String url;
|
final String url;
|
||||||
|
|||||||
@@ -45,4 +45,18 @@ class UserPrefs {
|
|||||||
/// 是否首次启动
|
/// 是否首次启动
|
||||||
bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true;
|
bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true;
|
||||||
Future<bool> setFirstLaunch(bool value) => prefs.setBool('isFirstLaunch', value);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,26 +42,9 @@ class BookListItem extends StatelessWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
// 评分
|
// 评分 - 5星显示
|
||||||
if (book.rating != null)
|
if (book.rating != null)
|
||||||
Row(
|
_buildStarRating(book.rating!)
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
const SizedBox(height: 16),
|
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) {
|
void _showDeleteDialog(BuildContext context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
|
|||||||
@@ -2,14 +2,36 @@ import 'dart:io';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import '../providers/app_provider.dart';
|
import '../providers/app_provider.dart';
|
||||||
import '../utils/user_prefs.dart';
|
import '../utils/user_prefs.dart';
|
||||||
import '../utils/toast_util.dart';
|
import '../utils/toast_util.dart';
|
||||||
import '../models/data_models.dart';
|
import '../models/data_models.dart';
|
||||||
|
|
||||||
/// 自定义左侧弹出菜单 - 极简主义设计
|
/// 自定义左侧弹出菜单 - 极简主义设计
|
||||||
class CustomDrawer extends StatelessWidget {
|
class CustomDrawer extends StatefulWidget {
|
||||||
CustomDrawer({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -43,9 +65,9 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
// 底部版本信息
|
// 底部版本信息
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'MookNote v0.1.5',
|
'MookNote v$_version',
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Color(0xFF999999),
|
color: Color(0xFF999999),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -42,26 +42,9 @@ class MovieListItem extends StatelessWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
// 评分
|
// 评分 - 5星显示
|
||||||
if (movie.rating != null)
|
if (movie.rating != null)
|
||||||
Row(
|
_buildStarRating(movie.rating!)
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
const SizedBox(height: 16),
|
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) {
|
void _showDeleteDialog(BuildContext context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@@ -464,6 +464,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.3.0"
|
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:
|
path:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: mooknote
|
name: mooknote
|
||||||
description: "A Flutter application for tracking movies, books, and notes"
|
description: "app for tracking movies, books, and notes"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.1.5+1
|
version: 0.1.6+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.0
|
sdk: ^3.5.0
|
||||||
@@ -27,6 +27,7 @@ dependencies:
|
|||||||
http: ^1.2.0
|
http: ^1.2.0
|
||||||
url_launcher: ^6.2.5
|
url_launcher: ^6.2.5
|
||||||
webview_flutter: ^4.8.0
|
webview_flutter: ^4.8.0
|
||||||
|
package_info_plus: ^8.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user