Files
MookNote/lib/pages/profile_page.dart
2026-05-23 17:25:48 +08:00

1667 lines
49 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
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';
import '../utils/user_prefs.dart';
import '../utils/toast_util.dart';
import 'recycle_bin_page.dart';
import 'sync/backup_page.dart';
import 'statistics_page.dart';
import 'sync/cloud_sync_page.dart';
import 'app_icon_picker_page.dart';
/// 个人中心页面 - 极简主义设计
class ProfilePage extends StatefulWidget {
const ProfilePage({super.key});
@override
State<ProfilePage> createState() => _ProfilePageState();
}
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;
});
}
/// 加载用户数据
Future<void> _loadUserData() async {
setState(() => _isLoading = true);
try {
await UserPrefs.init();
setState(() {
_nickname = _userPrefs.nickname;
_motto = _userPrefs.motto;
_avatarPath = _userPrefs.avatarPath;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFF1A1A1A),
),
);
}
return Column(
children: [
// 标题栏
AppBar(
title: const Text('我的'),
actions: [
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () => _showSettings(context),
),
],
),
// 内容
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 顶部用户信息
_buildUserHeader(),
const SizedBox(height: 8),
// 数据统计
_buildStatsSection(),
const SizedBox(height: 8),
// 功能菜单
_buildMenuSection(),
const SizedBox(height: 40),
// 底部留白,避免被 dock 栏遮挡
const SizedBox(height: 100),
],
),
),
),
],
);
}
/// 用户头部信息
Widget _buildUserHeader() {
return Container(
padding: const EdgeInsets.all(24),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 左侧头像
GestureDetector(
onTap: _pickAvatar,
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5),
shape: BoxShape.circle,
),
child: _avatarPath != null && _avatarPath!.isNotEmpty
? ClipOval(
child: Image.file(
File(_avatarPath!),
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildAvatarPlaceholder(),
),
)
: _buildAvatarPlaceholder(),
),
),
const SizedBox(width: 20),
// 右侧信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 昵称
GestureDetector(
onTap: () => _editNickname(context),
child: Text(
_nickname,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
),
const SizedBox(height: 6),
// 座右铭
GestureDetector(
onTap: () => _editMotto(context),
child: Text(
_motto,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF666666),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
);
}
Widget _buildAvatarPlaceholder() {
return const Center(
child: Icon(
Icons.person_outline,
size: 32,
color: Color(0xFFCCCCCC),
),
);
}
/// 数据统计区域
Widget _buildStatsSection() {
return Consumer<AppProvider>(
builder: (context, provider, child) {
final movies = provider.movies;
final books = provider.books;
final notes = provider.notes;
final movieCount = movies.where((m) => !m.isDeleted).length;
final bookCount = books.length;
final noteCount = notes.length;
return Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 24),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatCard('观影', movieCount, '', Icons.movie_outlined),
_buildStatCard('阅读', bookCount, '', Icons.menu_book_outlined),
_buildStatCard('笔记', noteCount, '', Icons.note_outlined),
],
),
),
);
},
);
}
/// 统计卡片
Widget _buildStatCard(String label, int count, String unit, IconData icon) {
return Column(
children: [
Icon(
icon,
size: 24,
color: const Color(0xFF666666),
),
const SizedBox(height: 8),
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
'$count',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w700,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(width: 2),
Text(
unit,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF666666),
),
),
],
),
const SizedBox(height: 2),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
],
);
}
/// 功能菜单
Widget _buildMenuSection() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
_buildMenuItem(
icon: Icons.analytics_outlined,
title: '数据统计',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const StatisticsPage()),
);
},
),
const Divider(height: 1, indent: 72, endIndent: 16, color: Color(0xFFE8E8E8)),
_buildMenuItem(
icon: Icons.backup_outlined,
title: '备份',
onTap: () => _showBackupOptions(context),
),
const Divider(height: 1, indent: 72, endIndent: 16, color: Color(0xFFE8E8E8)),
_buildMenuItem(
icon: Icons.delete_outline,
title: '回收站',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const RecycleBinPage()),
);
},
),
],
),
);
}
/// 菜单项
Widget _buildMenuItem({
required IconData icon,
required String title,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
// 图标背景
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
),
child: Icon(
icon,
size: 20,
color: const Color(0xFF666666),
),
),
const SizedBox(width: 16),
// 标题
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
),
),
// 箭头
const Icon(
Icons.chevron_right,
size: 20,
color: Color(0xFFCCCCCC),
),
],
),
),
);
}
/// 显示备份选项
void _showBackupOptions(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (ctx) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 36, height: 4,
decoration: BoxDecoration(
color: const Color(0xFFDDDDDD),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
const Align(
alignment: Alignment.centerLeft,
child: Text('选择备份方式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
),
const SizedBox(height: 16),
ListTile(
contentPadding: EdgeInsets.zero,
leading: Container(
width: 44, height: 44,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.folder_outlined, color: Color(0xFF666666)),
),
title: const Text('本地备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
subtitle: const Text('备份到本地文件夹,支持恢复', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
trailing: const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC)),
onTap: () {
Navigator.pop(ctx);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const BackupPage()),
).then((_) => _loadUserData());
},
),
const Divider(height: 0.5, color: Color(0xFFF0F0F0)),
ListTile(
contentPadding: EdgeInsets.zero,
leading: Container(
width: 44, height: 44,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.cloud_outlined, color: Color(0xFF666666)),
),
title: const Text('云备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
subtitle: const Text('通过 WebDAV 同步到云端', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
trailing: const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC)),
onTap: () {
Navigator.pop(ctx);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const CloudSyncPage()),
);
},
),
const SizedBox(height: 20),
],
),
),
);
}
/// 显示提示
void _showToast(String message) {
ToastUtil.show(context, message);
}
/// 选择头像
Future<void> _pickAvatar() async {
try {
final XFile? pickedFile = await _picker.pickImage(
source: ImageSource.gallery,
maxWidth: 400,
maxHeight: 400,
imageQuality: 85,
);
if (pickedFile != null) {
final appDir = await getApplicationDocumentsDirectory();
final fileName = 'avatar_${DateTime.now().millisecondsSinceEpoch}.jpg';
final savedPath = path.join(appDir.path, 'avatars', fileName);
final avatarDir = Directory(path.join(appDir.path, 'avatars'));
if (!await avatarDir.exists()) {
await avatarDir.create(recursive: true);
}
await File(pickedFile.path).copy(savedPath);
// 保存到本地存储
await _userPrefs.setAvatarPath(savedPath);
setState(() => _avatarPath = savedPath);
}
} catch (e) {
if (mounted) {
_showToast('选择头像失败: $e');
}
}
}
void _editNickname(BuildContext context) {
final controller = TextEditingController(text: _nickname);
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text(
'修改昵称',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
content: TextField(
controller: controller,
decoration: const InputDecoration(
hintText: '输入昵称',
border: UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFFE5E5E5)),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
final newNickname = controller.text.trim();
if (newNickname.isNotEmpty) {
await _userPrefs.setNickname(newNickname);
setState(() => _nickname = newNickname);
}
Navigator.pop(context);
},
child: const Text('确定'),
),
],
),
);
}
/// 编辑座右铭
void _editMotto(BuildContext context) {
final controller = TextEditingController(text: _motto);
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text(
'修改座右铭',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
content: TextField(
controller: controller,
maxLines: 2,
decoration: const InputDecoration(
hintText: '输入座右铭',
border: UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFFE5E5E5)),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
final newMotto = controller.text.trim();
await _userPrefs.setMotto(newMotto);
setState(() => _motto = newMotto);
Navigator.pop(context);
},
child: const Text('确定'),
),
],
),
);
}
/// 显示设置
void _showSettings(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsPage(),
),
);
}
}
/// 设置页面
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
final UserPrefs _userPrefs = UserPrefs();
bool _hideBottomNavOnScroll = true;
@override
void initState() {
super.initState();
_hideBottomNavOnScroll = _userPrefs.hideBottomNavOnScroll;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('设置'),
),
body: ListView(
children: [
// 数据管理
_buildSectionHeader('数据管理'),
_buildActionItem(
icon: Icons.cleaning_services_outlined,
title: '清除缓存数据',
subtitle: '清理未在数据库中引用的图片文件',
onTap: () => _showClearCacheDialog(context),
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
// 主界面功能显示入口
_buildSectionHeader('个性化设置'),
_buildSwitchItem(
icon: Icons.swipe_vertical_outlined,
title: '底部导航栏滚动隐藏',
subtitle: '下滑时自动隐藏底部导航栏',
value: _hideBottomNavOnScroll,
onChanged: _toggleHideBottomNavOnScroll,
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
_buildNavigationItem(
icon: Icons.apps_outlined,
title: '应用图标',
subtitle: '更换桌面应用图标',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AppIconPickerPage(),
),
);
},
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
_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('布局设置'),
_buildNavigationItem(
icon: Icons.dashboard_outlined,
title: '布局设置',
subtitle: '笔记、影视、阅读的展示样式',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const LayoutSettingsPage()),
);
},
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
// 使用说明
_buildSectionHeader('帮助'),
_buildLinkItem(
context: context,
icon: Icons.help_outline,
title: '使用说明',
subtitle: '查看应用使用指南',
url: 'https://mooknote.iletter.top/#/guide',
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
// 关于作者
// _buildLinkItem(
// context: context,
// icon: Icons.person_outline,
// title: '关于作者',
// subtitle: '了解更多信息',
// url: 'https://www.iletter.top/',
// ),
// const Divider(height: 0.5, indent: 24, endIndent: 24),
],
),
);
}
/// 切换底部导航栏滚动隐藏
Future<void> _toggleHideBottomNavOnScroll(bool value) async {
await _userPrefs.setHideBottomNavOnScroll(value);
setState(() => _hideBottomNavOnScroll = value);
}
/// 构建开关项
Widget _buildSwitchItem({
required IconData icon,
required String title,
required String subtitle,
required bool value,
required ValueChanged<bool> onChanged,
}) {
return InkWell(
onTap: () => onChanged(!value),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
color: const Color(0xFF666666),
size: 22,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
],
),
),
Switch(
value: value,
onChanged: onChanged,
activeColor: const Color(0xFF1A1A1A),
activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3),
inactiveThumbColor: Colors.white,
inactiveTrackColor: const Color(0xFFE5E5E5),
),
],
),
),
);
}
/// 构建区块标题
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 32, 24, 12),
child: Text(
title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
);
}
/// 构建导航项
Widget _buildNavigationItem({
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row(
children: [
// 图标背景
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
color: const Color(0xFF666666),
size: 22,
),
),
const SizedBox(width: 16),
// 文字内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
],
),
),
// 箭头
const Icon(
Icons.chevron_right,
color: Color(0xFFCCCCCC),
size: 20,
),
],
),
),
);
}
/// 构建链接项
Widget _buildLinkItem({
required BuildContext context,
required IconData icon,
required String title,
required String subtitle,
required String url,
}) {
return InkWell(
onTap: () => _launchUrl(context, url),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row(
children: [
// 图标背景
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
color: const Color(0xFF666666),
size: 22,
),
),
const SizedBox(width: 16),
// 文字内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
],
),
),
// 外部链接图标
const Icon(
Icons.open_in_new,
color: Color(0xFFCCCCCC),
size: 18,
),
],
),
),
);
}
/// 构建操作项(无箭头)
Widget _buildActionItem({
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row(
children: [
// 图标背景
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
color: const Color(0xFF666666),
size: 22,
),
),
const SizedBox(width: 16),
// 文字内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
],
),
),
],
),
),
);
}
/// 显示清除缓存对话框
void _showClearCacheDialog(BuildContext pageContext) {
showDialog(
context: pageContext,
builder: (dialogContext) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('清除缓存数据'),
content: const Text('这将删除所有未在数据库中引用的图片文件。确定要继续吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
Navigator.pop(dialogContext);
await _clearCacheData(pageContext);
},
child: const Text('确定', style: TextStyle(color: Colors.red)),
),
],
),
);
}
/// 清除缓存数据
Future<void> _clearCacheData(BuildContext context) async {
try {
// 显示进度提示
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
child: CircularProgressIndicator(),
),
);
// 获取所有数据库中的图片路径
final appProvider = context.read<AppProvider>();
final dbImagePaths = await _getAllDbImagePaths(appProvider);
// 清理图片目录
final deletedCount = await _cleanImageDirectory(dbImagePaths);
// 关闭进度提示
Navigator.pop(context);
// 显示结果
if (context.mounted) {
ToastUtil.show(context, '已清理 $deletedCount 个缓存文件');
}
} catch (e) {
// 关闭进度提示
Navigator.pop(context);
if (context.mounted) {
ToastUtil.show(context, '清理失败: $e');
}
}
}
/// 获取数据库中所有图片路径
Future<Set<String>> _getAllDbImagePaths(AppProvider provider) async {
final paths = <String>{};
// 获取所有影视的封面路径
final movies = provider.movies;
for (final movie in movies) {
final posterPath = movie.posterPath;
if (posterPath != null && posterPath.isNotEmpty) {
paths.add(posterPath);
}
}
// 获取所有书籍的封面路径
final books = provider.books;
for (final book in books) {
final coverPath = book.coverPath;
if (coverPath != null && coverPath.isNotEmpty) {
paths.add(coverPath);
}
}
// 获取所有笔记中的图片路径
final notes = provider.notes;
for (final note in notes) {
for (final imagePath in note.images) {
if (imagePath.isNotEmpty) {
paths.add(imagePath);
}
}
}
// 获取所有海报墙图片路径
final movieIds = movies.map((m) => m.id).toList();
for (final movieId in movieIds) {
final posters = await provider.getMoviePosters(movieId);
for (final poster in posters) {
final posterPath = poster.posterPath;
if (posterPath.isNotEmpty) {
paths.add(posterPath);
}
}
}
return paths;
}
/// 清理图片目录
Future<int> _cleanImageDirectory(Set<String> 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) {
final filePath = entity.path;
// 如果文件不在数据库中,删除它
if (!dbImagePaths.contains(filePath)) {
try {
await entity.delete();
deletedCount++;
} catch (e) {
// 忽略单个文件删除错误
}
}
}
}
} catch (e) {
debugPrint('清理图片目录失败: $e');
}
return deletedCount;
}
/// 打开链接(应用内打开)
void _launchUrl(BuildContext context, String url) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebViewPage(url: url),
),
);
}
}
/// 主界面功能显示设置页面
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;
int _defaultTabIndex = 0;
@override
void initState() {
super.initState();
_loadSettings();
}
/// 加载设置
void _loadSettings() {
setState(() {
_showMovieTab = _userPrefs.showMovieTab;
_showBookTab = _userPrefs.showBookTab;
_showNoteTab = _userPrefs.showNoteTab;
_defaultTabIndex = _userPrefs.defaultMainTabIndex;
});
}
/// 获取已启用的标签数量
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: [
// 启动设置
_buildSectionHeader('启动设置'),
_buildDefaultTabSelector(),
const Divider(height: 0.5, indent: 24, endIndent: 24),
// 模块开关
_buildSectionHeader('模块开关'),
_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),
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
child: const Text(
'至少保留一个模块,关闭后对应标签页将不再显示。',
style: TextStyle(fontSize: 12, color: Color(0xFFBBBBBB)),
),
),
],
),
);
}
/// 构建区块标题
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12),
child: Text(
title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
);
}
/// 构建默认启动标签选择器
Widget _buildDefaultTabSelector() {
final labels = ['影视', '阅读', '笔记'];
final icons = [Icons.movie_outlined, Icons.menu_book_outlined, Icons.note_outlined];
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
leading: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.home_outlined, color: Color(0xFF666666), size: 22),
),
title: const Text(
'默认启动标签',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)),
),
subtitle: const Text(
'打开应用时默认显示的页面',
style: TextStyle(fontSize: 12, color: Color(0xFF999999)),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
labels[_defaultTabIndex],
style: const TextStyle(fontSize: 14, color: Color(0xFF999999)),
),
const SizedBox(width: 4),
const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC), size: 20),
],
),
onTap: () => _showDefaultTabPicker(labels, icons),
);
}
void _showDefaultTabPicker(List<String> labels, List<IconData> icons) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (ctx) => Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: 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: const Color(0xFFDDDDDD), borderRadius: BorderRadius.circular(2)),
),
const SizedBox(height: 20),
const Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Text('默认启动标签', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
),
),
const SizedBox(height: 16),
for (int i = 0; i < labels.length; i++)
ListTile(
leading: Container(
width: 44, height: 44,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icons[i], color: const Color(0xFF666666)),
),
title: Text(labels[i], style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
trailing: _defaultTabIndex == i ? const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 20) : null,
onTap: () async {
await _userPrefs.setDefaultMainTabIndex(i);
setState(() => _defaultTabIndex = i);
Navigator.pop(ctx);
},
),
const SizedBox(height: 8),
],
),
),
);
}
/// 构建开关项
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),
),
);
}
}
/// 布局设置页面
class LayoutSettingsPage extends StatefulWidget {
const LayoutSettingsPage({super.key});
@override
State<LayoutSettingsPage> createState() => _LayoutSettingsPageState();
}
class _LayoutSettingsPageState extends State<LayoutSettingsPage> {
final UserPrefs _userPrefs = UserPrefs();
int _noteLayout = 0;
int _movieLayout = 0;
int _bookLayout = 0;
@override
void initState() {
super.initState();
_noteLayout = _userPrefs.noteLayoutStyle;
_movieLayout = _userPrefs.movieLayoutStyle;
_bookLayout = _userPrefs.bookLayoutStyle;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('布局设置'),
),
body: ListView(
children: [
// 笔记布局
_buildSectionHeader('笔记布局'),
_buildLayoutOption(
icon: Icons.view_list_outlined,
title: '列表布局',
subtitle: '单列列表,简洁清晰',
value: 0,
groupValue: _noteLayout,
onTap: () => _setLayout('note', 0),
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
_buildLayoutOption(
icon: Icons.grid_view_outlined,
title: '瀑布流布局',
subtitle: '双列卡片,图文并茂',
value: 1,
groupValue: _noteLayout,
onTap: () => _setLayout('note', 1),
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
_buildLayoutOption(
icon: Icons.timeline_outlined,
title: '时间线布局',
subtitle: '按时间排列,纵览全局',
value: 2,
groupValue: _noteLayout,
onTap: () => _setLayout('note', 2),
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
// 影视布局
_buildSectionHeader('影视布局'),
_buildLayoutOption(
icon: Icons.grid_view_outlined,
title: '海报网格',
subtitle: '三列海报,赏心悦目',
value: 0,
groupValue: _movieLayout,
onTap: () => _setLayout('movie', 0),
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
_buildLayoutOption(
icon: Icons.view_list_outlined,
title: '列表布局',
subtitle: '单列卡片,信息一览',
value: 1,
groupValue: _movieLayout,
onTap: () => _setLayout('movie', 1),
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
// 阅读布局
_buildSectionHeader('阅读布局'),
_buildLayoutOption(
icon: Icons.grid_view_outlined,
title: '封面网格',
subtitle: '三列封面,清新雅致',
value: 0,
groupValue: _bookLayout,
onTap: () => _setLayout('book', 0),
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
_buildLayoutOption(
icon: Icons.view_list_outlined,
title: '列表布局',
subtitle: '单列卡片,信息一览',
value: 1,
groupValue: _bookLayout,
onTap: () => _setLayout('book', 1),
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
],
),
);
}
void _setLayout(String type, int value) async {
switch (type) {
case 'note':
await _userPrefs.setNoteLayoutStyle(value);
setState(() => _noteLayout = value);
break;
case 'movie':
await _userPrefs.setMovieLayoutStyle(value);
setState(() => _movieLayout = value);
break;
case 'book':
await _userPrefs.setBookLayoutStyle(value);
setState(() => _bookLayout = value);
break;
}
}
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12),
child: Text(
title,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
),
);
}
Widget _buildLayoutOption({
required IconData icon,
required String title,
required String subtitle,
required int value,
required int groupValue,
required VoidCallback onTap,
}) {
final selected = value == groupValue;
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
leading: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: const Color(0xFF666666), size: 22),
),
title: Text(
title,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)),
),
subtitle: Text(
subtitle,
style: const TextStyle(fontSize: 12, color: Color(0xFF999999)),
),
trailing: selected
? const Icon(Icons.check_circle, color: Color(0xFF1A1A1A), size: 20)
: const Icon(Icons.circle_outlined, color: Color(0xFFDDDDDD), size: 20),
onTap: onTap,
);
}
}
/// WebView 页面
class WebViewPage extends StatefulWidget {
final String url;
const WebViewPage({super.key, required this.url});
@override
State<WebViewPage> createState() => _WebViewPageState();
}
class _WebViewPageState extends State<WebViewPage> {
late final WebViewController _controller;
bool _isLoading = true;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (String url) {
setState(() {
_isLoading = true;
});
},
onPageFinished: (String url) {
setState(() {
_isLoading = false;
});
},
onWebResourceError: (WebResourceError error) {
setState(() {
_isLoading = false;
});
},
),
)
..loadRequest(Uri.parse(widget.url));
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text(''),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => _controller.reload(),
),
],
),
body: Stack(
children: [
WebViewWidget(controller: _controller),
if (_isLoading)
const Center(
child: CircularProgressIndicator(
color: Color(0xFF999999),
),
),
],
),
);
}
}