generated from dellevin/template
1449 lines
41 KiB
Dart
1449 lines
41 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),
|
|
|
|
// 版本信息
|
|
Center(
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF5F5F5),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
'MookNote v$_version',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF666666),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// 底部留白,避免被 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 watchedCount = movies.where((m) => m.status == 'watched' && !m.isDeleted).length;
|
|
final watchingCount = movies.where((m) => m.status == 'watching' && !m.isDeleted).length;
|
|
final wantToWatchCount = movies.where((m) => m.status == 'want_to_watch' && !m.isDeleted).length;
|
|
|
|
final bookCount = books.length;
|
|
final readCount = books.where((b) => b.status == 'read').length;
|
|
final readingCount = books.where((b) => b.status == 'reading').length;
|
|
final wantToReadCount = books.where((b) => b.status == 'want_to_read').length;
|
|
|
|
final noteCount = notes.length;
|
|
|
|
final movieRatings = movies
|
|
.where((m) => m.rating != null && !m.isDeleted)
|
|
.map((m) => m.rating!);
|
|
final avgMovieRating = movieRatings.isNotEmpty
|
|
? movieRatings.reduce((a, b) => a + b) / movieRatings.length
|
|
: 0.0;
|
|
|
|
final bookRatings = books
|
|
.where((b) => b.rating != null)
|
|
.map((b) => b.rating!);
|
|
final avgBookRating = bookRatings.isNotEmpty
|
|
? bookRatings.reduce((a, b) => a + b) / bookRatings.length
|
|
: 0.0;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(24, 16, 24, 24),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 主统计卡片
|
|
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),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// 详情统计
|
|
_buildSectionTitle('观影详情'),
|
|
const SizedBox(height: 12),
|
|
_buildDetailGrid([
|
|
_buildDetailItem('已看', watchedCount, Icons.check_circle_outline),
|
|
_buildDetailItem('在看', watchingCount, Icons.play_circle_outline),
|
|
_buildDetailItem('想看', wantToWatchCount, Icons.bookmark_border),
|
|
_buildDetailItem('均分', avgMovieRating > 0 ? avgMovieRating.toStringAsFixed(1) : '-', Icons.star_outline),
|
|
]),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
_buildSectionTitle('阅读详情'),
|
|
const SizedBox(height: 12),
|
|
_buildDetailGrid([
|
|
_buildDetailItem('已读', readCount, Icons.check_circle_outline),
|
|
_buildDetailItem('在读', readingCount, Icons.play_circle_outline),
|
|
_buildDetailItem('想读', wantToReadCount, Icons.bookmark_border),
|
|
_buildDetailItem('均分', avgBookRating > 0 ? avgBookRating.toStringAsFixed(1) : '-', Icons.star_outline),
|
|
]),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 统计卡片
|
|
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 _buildDetailGrid(List<Widget> children) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFFAFAFA),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: children,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 详情项
|
|
Widget _buildDetailItem(String label, dynamic value, IconData icon) {
|
|
return Column(
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
size: 18,
|
|
color: const Color(0xFF999999),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'$value',
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(0xFF1A1A1A),
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
color: Color(0xFF999999),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 区块标题
|
|
Widget _buildSectionTitle(String title) {
|
|
return Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
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: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (context) => const BackupPage()),
|
|
).then((_) {
|
|
// 返回时刷新用户数据
|
|
_loadUserData();
|
|
});
|
|
},
|
|
),
|
|
const Divider(height: 1, indent: 72, endIndent: 16, color: Color(0xFFE8E8E8)),
|
|
|
|
_buildMenuItem(
|
|
icon: Icons.cloud_sync_outlined,
|
|
title: '云备份',
|
|
onTap: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (context) => const CloudSyncPage()),
|
|
);
|
|
},
|
|
),
|
|
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 _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('帮助'),
|
|
_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;
|
|
|
|
@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;
|
|
|
|
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),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|