适配深色模式

This commit is contained in:
DelLevin-Home
2026-05-26 05:10:21 +08:00
parent b36f50827f
commit 73f3795e65
53 changed files with 3846 additions and 3211 deletions

View File

@@ -1,8 +1,9 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'dart:async';
import 'pages/home_page.dart';
import 'utils/theme/app_theme.dart';
import 'utils/app_router.dart';
@@ -13,31 +14,17 @@ import 'providers/app_provider.dart';
import 'package:flutter/widget_previews.dart';
void main() async {
// 确保 Flutter 绑定初始化完成
WidgetsFlutterBinding.ensureInitialized();
// 设置系统导航栏颜色与App主题一致
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.white,
systemNavigationBarIconBrightness: Brightness.dark,
),
);
// 初始化用户偏好设置
await UserPrefs.init();
final appProvider = AppProvider();
// 先显示界面,后台加载数据
runApp(MyApp(appProvider: appProvider));
unawaited(appProvider.initDatabase().then((_) => appProvider.initMainTabIndex()));
unawaited(_initAutoBackup());
unawaited(_initUsageStats());
}
/// 初始化自动备份
Future<void> _initAutoBackup() async {
try {
// 初始化本地自动备份
final isLocalAutoBackupEnabled = await AutoBackupService.instance.getEnabled();
if (isLocalAutoBackupEnabled) {
await AutoBackupService.instance.start();
@@ -47,7 +34,6 @@ Future<void> _initAutoBackup() async {
}
}
/// 初始化匿名用户统计
Future<void> _initUsageStats() async {
try {
await UsageStatsService.instance.start();
@@ -56,51 +42,92 @@ Future<void> _initUsageStats() async {
}
}
class MyApp extends StatelessWidget {
class MyApp extends StatefulWidget {
final AppProvider appProvider;
const MyApp({super.key, required this.appProvider});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
widget.appProvider.loadThemeMode();
_updateSystemUI(widget.appProvider.themeMode);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_updateSystemUI(widget.appProvider.themeMode);
}
}
void _updateSystemUI(ThemeMode mode) {
final Brightness brightness;
switch (mode) {
case ThemeMode.light:
brightness = Brightness.light;
case ThemeMode.dark:
brightness = Brightness.dark;
case ThemeMode.system:
brightness = PlatformDispatcher.instance.platformBrightness;
}
final isDark = brightness == Brightness.dark;
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: isDark ? const Color(0xFF1A1A1A) : Colors.white,
systemNavigationBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
));
}
@override
Widget build(BuildContext context) {
// 获取当前选中的图标名称
final iconName = UserPrefs().appIconName;
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: appProvider),
ChangeNotifierProvider.value(value: widget.appProvider),
],
child: MaterialApp(
title: 'MookNote',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('zh', 'CN'),
Locale('en', 'US'),
],
home: const HomePage(),
onGenerateRoute: AppRouter.generateRoute,
builder: (context, child) {
// 尝试动态设置应用图标Android 13+ 支持动态图标,但 Flutter 目前主要通过静态配置)
// 这里我们主要实现逻辑上的切换,实际生效通常需要重启应用或配合原生插件
return _AppIconWrapper(iconName: iconName, child: child!);
child: Consumer<AppProvider>(
builder: (context, provider, _) {
_updateSystemUI(provider.themeMode);
return MaterialApp(
title: 'MookNote',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: provider.themeMode,
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('zh', 'CN'),
Locale('en', 'US'),
],
home: const HomePage(),
onGenerateRoute: AppRouter.generateRoute,
builder: (context, child) {
return _AppIconWrapper(iconName: iconName, child: child!);
},
);
},
),
);
}
}
/// 应用图标包装器
/// 注意Flutter 默认不支持运行时动态更换桌面图标。
/// 这里的实现主要是为了在应用内记录用户的选择,并为未来可能的动态图标功能做准备。
/// 如果需要真正的动态图标,通常需要引入 flutter_app_icon_changer 等插件并配置多套图标资源。
class _AppIconWrapper extends StatefulWidget {
final Widget child;
final String iconName;
@@ -118,11 +145,7 @@ class _AppIconWrapperState extends State<_AppIconWrapper> {
_updateSystemIcon();
}
Future<void> _updateSystemIcon() async {
// 目前 Flutter 官方不支持直接通过代码更换 Launcher Icon。
// 这一步主要用于记录日志或在未来集成第三方库时使用。
// print('Current selected icon: ${widget.iconName}');
}
Future<void> _updateSystemIcon() async {}
@override
Widget build(BuildContext context) {
@@ -130,11 +153,9 @@ class _AppIconWrapperState extends State<_AppIconWrapper> {
}
}
/// 用于预览 MyApp 的 Widget
/// 添加 @Preview 注解
@Preview(name: "MookNote App Preview")
Widget previewMyApp() {
final appProvider = AppProvider();
final appProvider = AppProvider();
return MultiProvider(
providers: [
@@ -142,4 +163,4 @@ Widget previewMyApp() {
],
child: MyApp(appProvider: appProvider),
);
}
}

View File

@@ -15,7 +15,6 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
final UserPrefs _userPrefs = UserPrefs();
String _currentIconName = 'app_icon';
// 预定义的图标列表
final List<Map<String, String>> _icons = [
{'name': 'app_icon', 'label': '默认图标'},
{'name': 'app_icon2', 'label': '风格二'},
@@ -28,7 +27,6 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
}
Future<void> _loadCurrentIcon() async {
// 先从原生层获取当前实际启用的图标(更准确)
final nativeIcon = await AppIconChannel.getCurrentIcon();
setState(() {
_currentIconName = nativeIcon;
@@ -39,7 +37,6 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
if (iconName == _currentIconName) return;
try {
// 调用原生层切换桌面图标
final success = await AppIconChannel.switchIcon(iconName);
if (success) {
@@ -65,8 +62,9 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: const Text('应用图标'),
),
@@ -82,22 +80,21 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
itemBuilder: (context, index) {
final icon = _icons[index];
final isSelected = _currentIconName == icon['name'];
return GestureDetector(
onTap: () => _selectIcon(icon['name']!),
child: Container(
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFF0F0F0) : const Color(0xFFFAFAFA),
color: isSelected ? colors.outlineVariant : colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFE8E8E8),
color: isSelected ? colors.primary : colors.outlineVariant,
width: isSelected ? 2 : 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 图标预览
Image.asset(
'assets/icon/${icon['name']}.png',
width: 64,
@@ -107,26 +104,25 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
width: 64,
height: 64,
decoration: BoxDecoration(
color: const Color(0xFFEEEEEE),
color: colors.outlineVariant,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.image_not_supported, color: Color(0xFF999999)),
child: Icon(Icons.image_not_supported, color: colors.onSurface.withValues(alpha: 0.4)),
);
},
),
const SizedBox(height: 12),
// 标签
Text(
icon['label']!,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: const Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
if (isSelected) ...[
const SizedBox(height: 4),
const Icon(Icons.check_circle, size: 18, color: Color(0xFF1A1A1A)),
Icon(Icons.check_circle, size: 18, color: colors.primary),
],
],
),

View File

@@ -13,9 +13,9 @@ import 'book_share_page.dart';
/// 书籍详情页 - 极简主义设计
class BookDetailPage extends StatefulWidget {
final Book book;
const BookDetailPage({super.key, required this.book});
@override
State<BookDetailPage> createState() => _BookDetailPageState();
}
@@ -24,80 +24,54 @@ class _BookDetailPageState extends State<BookDetailPage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 页面获得焦点时刷新数据
_refreshBookData();
}
void _refreshBookData() {
final provider = context.read<AppProvider>();
// 强制刷新当前书籍数据
provider.loadBooks();
}
@override
Widget build(BuildContext context) {
// 从 Provider 获取最新的 book 数据,实现动态刷新
final colors = Theme.of(context).colorScheme;
final book = context.watch<AppProvider>().books
.where((b) => b.id == widget.book.id)
.firstOrNull ?? widget.book;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
body: Stack(
children: [
CustomScrollView(
slivers: [
// 顶部封面区域
_buildSliverAppBar(book),
// 内容区域
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 基本信息
_buildBasicInfo(book),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 作者信息
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildAuthorsSection(book),
// 类型
if (book.genres.isNotEmpty)
_buildGenresSection(book),
// ISBN
if (book.isbn != null && book.isbn!.isNotEmpty)
_buildIsbnSection(book),
// 出版社
if (book.publisher != null && book.publisher!.isNotEmpty)
_buildPublisherSection(book),
// 出版时间
if (book.publishDate != null)
_buildPublishDateSection(book),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 简介
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
if (book.summary != null && book.summary!.isNotEmpty)
_buildSummarySection(book),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 书评和摘抄入口
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildExtraSections(book),
const SizedBox(height: 120),
],
),
),
],
),
// 右下角悬浮按钮组
Positioned(
right: 16,
bottom: 24,
@@ -107,9 +81,9 @@ class _BookDetailPageState extends State<BookDetailPage> {
),
);
}
/// 构建右下角悬浮按钮组
Widget _buildFloatingActionButtons(Book book) {
final colors = Theme.of(context).colorScheme;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -117,13 +91,16 @@ class _BookDetailPageState extends State<BookDetailPage> {
icon: Icons.edit_outlined,
onPressed: () => _navigateToEdit(context),
tooltip: '编辑',
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
),
const SizedBox(height: 12),
_buildFloatingButton(
icon: Icons.delete_outline,
onPressed: () => _showDeleteDialog(context),
tooltip: '删除',
backgroundColor: Colors.red,
backgroundColor: colors.error,
foregroundColor: colors.onError,
),
const SizedBox(height: 12),
_buildFloatingButton(
@@ -131,17 +108,18 @@ class _BookDetailPageState extends State<BookDetailPage> {
onPressed: () => _showSharePoster(book),
tooltip: '分享海报',
backgroundColor: const Color(0xFF4CAF50),
foregroundColor: Colors.white,
),
],
);
}
/// 构建单个悬浮按钮
Widget _buildFloatingButton({
required IconData icon,
required VoidCallback onPressed,
required String tooltip,
Color backgroundColor = const Color(0xFF1A1A1A),
required Color backgroundColor,
required Color foregroundColor,
}) {
return Material(
color: Colors.transparent,
@@ -160,15 +138,15 @@ class _BookDetailPageState extends State<BookDetailPage> {
],
),
child: IconButton(
icon: Icon(icon, size: 18, color: Colors.white),
icon: Icon(icon, size: 18, color: foregroundColor),
onPressed: onPressed,
padding: EdgeInsets.zero,
tooltip: tooltip,
),
),
);
}
/// 构建带背景的返回按钮
Widget _buildBackButton() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
@@ -194,21 +172,19 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
/// 构建顶部 AppBar
Widget _buildSliverAppBar(Book book) {
final colors = Theme.of(context).colorScheme;
return SliverAppBar(
expandedHeight: 320,
pinned: true,
backgroundColor: const Color(0xFFF5F5F5),
backgroundColor: colors.surfaceContainerHighest,
leading: _buildBackButton(),
flexibleSpace: FlexibleSpaceBar(
background: _buildCoverSection(book),
),
// 右上角按钮已移到右下角悬浮按钮
);
}
/// 构建封面区域
Widget _buildCoverSection(Book book) {
return SizedBox.expand(
child: book.coverPath != null && book.coverPath!.isNotEmpty
@@ -220,79 +196,74 @@ class _BookDetailPageState extends State<BookDetailPage> {
: _buildCoverPlaceholder(),
);
}
Widget _buildCoverPlaceholder() {
return const Center(
final colors = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.menu_book,
size: 64,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
),
SizedBox(height: 16),
const SizedBox(height: 16),
Text(
'暂无封面',
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
),
);
}
/// 构建基本信息
Widget _buildBasicInfo(Book book) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 书名
Text(
book.title,
style: const TextStyle(
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.3,
),
),
// 别名(显示在主名称下面,用 / 分隔)
if (book.alternateTitles.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
book.alternateTitles.join(' / '),
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
height: 1.4,
),
),
],
const SizedBox(height: 16),
// 评分和状态
Row(
children: [
if (book.rating != null) ...[
const Icon(
Icon(
Icons.star,
size: 20,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
const SizedBox(width: 4),
Text(
book.rating!.toStringAsFixed(1),
style: const TextStyle(
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
const SizedBox(width: 16),
@@ -300,15 +271,12 @@ class _BookDetailPageState extends State<BookDetailPage> {
_buildStatusTag(book),
],
),
const SizedBox(height: 8),
// 时间信息
Text(
'添加于 ${_formatDate(book.createdAt)}',
style: const TextStyle(
style: TextStyle(
fontSize: 12,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
@@ -316,33 +284,33 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
/// 构建状态标签
Widget _buildStatusTag(Book book) {
final colors = Theme.of(context).colorScheme;
String label;
Color bgColor;
Color textColor;
switch (book.status) {
case 'read':
label = '已读';
bgColor = const Color(0xFF1A1A1A);
textColor = Colors.white;
bgColor = colors.primary;
textColor = colors.onPrimary;
break;
case 'reading':
label = '在读';
bgColor = const Color(0xFFF0F0F0);
textColor = const Color(0xFF666666);
bgColor = colors.outlineVariant;
textColor = colors.onSurface.withValues(alpha: 0.6);
break;
case 'want_to_read':
label = '想读';
bgColor = const Color(0xFFF5F5F5);
textColor = const Color(0xFF999999);
bgColor = colors.surfaceContainerHighest;
textColor = colors.onSurface.withValues(alpha: 0.4);
break;
default:
label = '未知';
bgColor = const Color(0xFFEEEEEE);
textColor = const Color(0xFFCCCCCC);
bgColor = colors.outlineVariant;
textColor = colors.onSurface.withValues(alpha: 0.25);
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
@@ -359,30 +327,30 @@ class _BookDetailPageState extends State<BookDetailPage> {
),
);
}
/// 构建作者区域
Widget _buildAuthorsSection(Book book) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
SizedBox(
width: 64,
child: Text(
'作者',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
),
Expanded(
child: Text(
book.authors.join(''),
style: const TextStyle(
style: TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.5,
),
),
@@ -392,29 +360,29 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
/// 构建ISBN区域
Widget _buildIsbnSection(Book book) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
SizedBox(
width: 64,
child: Text(
'ISBN',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
),
Expanded(
child: Text(
book.isbn!,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.5,
),
),
@@ -424,29 +392,29 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
/// 构建出版社区域
Widget _buildPublisherSection(Book book) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
SizedBox(
width: 64,
child: Text(
'出版社',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
),
Expanded(
child: Text(
book.publisher!,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.5,
),
),
@@ -456,29 +424,29 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
/// 构建出版时间区域
Widget _buildPublishDateSection(Book book) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
SizedBox(
width: 64,
child: Text(
'出版时间',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
),
Expanded(
child: Text(
'${book.publishDate!.year}${book.publishDate!.month.toString().padLeft(2, '0')}',
style: const TextStyle(
style: TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.5,
),
),
@@ -488,20 +456,20 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
/// 构建类型区域
Widget _buildGenresSection(Book book) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
SizedBox(
width: 64,
child: Text(
'类型',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
),
@@ -513,14 +481,14 @@ class _BookDetailPageState extends State<BookDetailPage> {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
child: Text(
genre,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
);
@@ -531,9 +499,9 @@ class _BookDetailPageState extends State<BookDetailPage> {
),
);
}
/// 构建简介区域
Widget _buildSummarySection(Book book) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
@@ -545,17 +513,17 @@ class _BookDetailPageState extends State<BookDetailPage> {
width: 4,
height: 16,
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.onSurface,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
const Text(
Text(
'简介',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
],
@@ -564,14 +532,14 @@ class _BookDetailPageState extends State<BookDetailPage> {
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: Text(
book.summary!,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.8,
),
),
@@ -581,8 +549,8 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
/// 构建额外功能区域(书评、摘抄)
Widget _buildExtraSections(Book book) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
@@ -594,23 +562,22 @@ class _BookDetailPageState extends State<BookDetailPage> {
width: 4,
height: 16,
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.onSurface,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
const Text(
Text(
'更多',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
],
),
const SizedBox(height: 16),
// 书评入口
_buildExtraSectionItem(
icon: Icons.rate_review_outlined,
title: '书评',
@@ -620,7 +587,6 @@ class _BookDetailPageState extends State<BookDetailPage> {
onTap: () => _navigateToReviews(book),
),
const SizedBox(height: 12),
// 摘抄入口
_buildExtraSectionItem(
icon: Icons.format_quote_outlined,
title: '摘抄',
@@ -634,7 +600,6 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
/// 构建更多区域项
Widget _buildExtraSectionItem({
required IconData icon,
required String title,
@@ -643,14 +608,15 @@ class _BookDetailPageState extends State<BookDetailPage> {
required String unit,
required VoidCallback onTap,
}) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Row(
children: [
@@ -658,14 +624,14 @@ class _BookDetailPageState extends State<BookDetailPage> {
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Icon(
icon,
size: 20,
color: const Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(width: 12),
@@ -675,10 +641,10 @@ class _BookDetailPageState extends State<BookDetailPage> {
children: [
Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
const SizedBox(height: 4),
@@ -688,9 +654,9 @@ class _BookDetailPageState extends State<BookDetailPage> {
final count = snapshot.data ?? 0;
return Text(
count > 0 ? '$count $unit' : emptyText,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
);
},
@@ -698,9 +664,9 @@ class _BookDetailPageState extends State<BookDetailPage> {
],
),
),
const Icon(
Icon(
Icons.chevron_right,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
),
],
),
@@ -726,39 +692,37 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
/// 格式化日期
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
/// 跳转到编辑页面
void _navigateToEdit(BuildContext context) {
Navigator.pushNamed(context, '/book-form', arguments: widget.book).then((_) {
context.read<AppProvider>().loadBooks();
});
}
/// 显示删除对话框
void _showDeleteDialog(BuildContext context) {
final colors = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text(
title: Text(
'确认删除',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
content: Text(
'确定要删除"${widget.book.title}"吗?删除后可在回收站恢复。',
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.5,
),
),
@@ -766,7 +730,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF666666),
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('取消'),
@@ -780,8 +744,8 @@ class _BookDetailPageState extends State<BookDetailPage> {
ToastUtil.show(context, '已删除');
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
backgroundColor: colors.error,
foregroundColor: colors.onError,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
@@ -796,7 +760,6 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
/// 下载封面到本地
Future<void> _downloadCover(Book book) async {
if (book.coverPath == null || book.coverPath!.isEmpty) {
ToastUtil.show(context, '没有可下载的封面');
@@ -810,16 +773,13 @@ class _BookDetailPageState extends State<BookDetailPage> {
return;
}
// 生成文件名书籍名称_时间戳_封面.jpg
final timestamp = DateTime.now().millisecondsSinceEpoch;
final fileName = '${book.title}_${timestamp}_封面.jpg';
// 获取临时目录路径
final tempDir = await Directory.systemTemp.createTemp();
final tempFile = File('${tempDir.path}/$fileName');
await sourceFile.copy(tempFile.path);
// 使用分享功能让用户选择保存位置
await Share.shareXFiles(
[XFile(tempFile.path)],
subject: '${book.title} 封面',
@@ -830,7 +790,6 @@ class _BookDetailPageState extends State<BookDetailPage> {
}
}
/// 显示分享海报页面
void _showSharePoster(Book book) {
Navigator.push(
context,

View File

@@ -50,8 +50,9 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: Text(_isEditing ? '编辑摘抄' : '添加摘抄'),
actions: [
@@ -63,10 +64,10 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text(
: Text(
'保存',
style: TextStyle(
color: Color(0xFF1A1A1A),
color: colors.onSurface,
fontWeight: FontWeight.w600,
),
),
@@ -82,15 +83,15 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
// 章节
TextFormField(
controller: _chapterController,
decoration: const InputDecoration(
decoration: InputDecoration(
labelText: '章节(可选)',
hintText: '例如第一章、第3节等',
border: OutlineInputBorder(
border: const OutlineInputBorder(
borderRadius: BorderRadius.zero,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero,
borderSide: BorderSide(color: Color(0xFF1A1A1A)),
borderSide: BorderSide(color: colors.primary),
),
),
),
@@ -101,16 +102,16 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
TextFormField(
controller: _contentController,
maxLines: 8,
decoration: const InputDecoration(
decoration: InputDecoration(
labelText: '摘抄内容',
hintText: '输入你想要摘抄的内容...',
alignLabelWithHint: true,
border: OutlineInputBorder(
border: const OutlineInputBorder(
borderRadius: BorderRadius.zero,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero,
borderSide: BorderSide(color: Color(0xFF1A1A1A)),
borderSide: BorderSide(color: colors.primary),
),
),
validator: (value) {
@@ -127,16 +128,16 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
TextFormField(
controller: _commentController,
maxLines: 5,
decoration: const InputDecoration(
decoration: InputDecoration(
labelText: '我的感悟(可选)',
hintText: '记录你对这段内容的思考和感悟...',
alignLabelWithHint: true,
border: OutlineInputBorder(
border: const OutlineInputBorder(
borderRadius: BorderRadius.zero,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero,
borderSide: BorderSide(color: Color(0xFF1A1A1A)),
borderSide: BorderSide(color: colors.primary),
),
),
),
@@ -185,4 +186,3 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
}
}
}

View File

@@ -41,8 +41,9 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: const Text('摘抄'),
actions: [
@@ -62,6 +63,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
}
Widget _buildEmptyState() {
final colors = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -70,21 +72,21 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
child: Icon(
Icons.format_quote_outlined,
size: 40,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
),
),
const SizedBox(height: 20),
const Text(
Text(
'暂无摘抄',
style: TextStyle(
fontSize: 16,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(height: 24),
@@ -93,15 +95,15 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
child: Text(
'添加记录',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
color: colors.onPrimary,
),
),
),
@@ -114,7 +116,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
/// 按章节分组摘抄数据
Map<String, List<BookExcerpt>> _groupExcerptsByChapter() {
final Map<String, List<BookExcerpt>> groups = {};
for (final excerpt in _excerpts) {
final chapter = excerpt.chapter.isEmpty ? '未分类' : excerpt.chapter;
if (!groups.containsKey(chapter)) {
@@ -122,19 +124,19 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
}
groups[chapter]!.add(excerpt);
}
// 每个章节内的摘抄按时间排序(新的在前)
for (final chapter in groups.keys) {
groups[chapter]!.sort((a, b) => b.createdAt.compareTo(a.createdAt));
}
return groups;
}
Widget _buildExcerptList() {
final groups = _groupExcerptsByChapter();
final chapters = groups.keys.toList();
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: chapters.length,
@@ -147,6 +149,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
}
Widget _buildChapterSection(String chapter, List<BookExcerpt> excerpts) {
final colors = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -154,18 +157,18 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: const BoxDecoration(
color: Color(0xFFF5F5F5),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
border: Border(
left: BorderSide(color: Color(0xFF1A1A1A), width: 4),
left: BorderSide(color: colors.primary, width: 4),
),
),
child: Text(
chapter,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
),
@@ -178,11 +181,12 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
}
Widget _buildExcerptItem(BookExcerpt excerpt) {
final colors = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)),
border: Border.all(color: colors.outline),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -192,9 +196,9 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
excerpt.content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.5,
),
),
@@ -205,16 +209,16 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: const BoxDecoration(
color: Color(0xFFF5F5F5),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
),
child: Text(
excerpt.comment,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
),
@@ -227,29 +231,29 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
children: [
Text(
_formatDate(excerpt.createdAt),
style: const TextStyle(
style: TextStyle(
fontSize: 11,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const Spacer(),
// 编辑按钮
GestureDetector(
onTap: () => _navigateToEditExcerpt(excerpt),
child: const Icon(
child: Icon(
Icons.edit_outlined,
size: 16,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(width: 12),
// 删除按钮
GestureDetector(
onTap: () => _showDeleteDialog(excerpt),
child: const Icon(
child: Icon(
Icons.delete_outline,
size: 16,
color: Colors.red,
color: colors.error,
),
),
],
@@ -287,28 +291,31 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
void _showDeleteDialog(BookExcerpt excerpt) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'),
content: const Text('确定要删除这条摘抄吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeBookExcerpt(excerpt.id);
Navigator.pop(context);
_loadExcerpts();
ToastUtil.show(context, '已删除');
},
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),
builder: (context) {
final colors = Theme.of(context).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'),
content: const Text('确定要删除这条摘抄吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeBookExcerpt(excerpt.id);
Navigator.pop(context);
_loadExcerpts();
ToastUtil.show(context, '删除');
},
child: Text('删除', style: TextStyle(color: colors.error)),
),
],
);
},
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -49,8 +49,9 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: const Text('书评详情'),
actions: [
@@ -70,9 +71,9 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
// 书评内容
Text(
_review.content,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.8,
),
),
@@ -82,7 +83,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
// 分隔线
Container(
height: 0.5,
color: const Color(0xFFE5E5E5),
color: colors.outline,
),
const SizedBox(height: 24),
@@ -92,6 +93,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
icon: Icons.person_outline,
label: '书评人:',
value: _review.reviewer.isNotEmpty ? _review.reviewer : '匿名',
colors: colors,
),
const SizedBox(height: 16),
@@ -102,6 +104,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
icon: Icons.source_outlined,
label: '来源:',
value: _review.source,
colors: colors,
),
if (_review.source.isNotEmpty) const SizedBox(height: 16),
@@ -111,6 +114,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
icon: Icons.category_outlined,
label: '类型:',
value: _review.typeText,
colors: colors,
),
const SizedBox(height: 16),
@@ -120,6 +124,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
icon: Icons.access_time,
label: '时间:',
value: _formatDate(_review.createdAt),
colors: colors,
),
],
),
@@ -132,29 +137,30 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
required IconData icon,
required String label,
required String value,
required ColorScheme colors,
}) {
return Row(
children: [
Icon(
icon,
size: 20,
color: const Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
const SizedBox(width: 12),
Text(
label,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
value,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
),

View File

@@ -47,10 +47,11 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final isEdit = widget.review != null;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: Text(isEdit ? '编辑书评' : '写书评'),
actions: [
@@ -74,24 +75,24 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
// 顶部信息栏
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: const BoxDecoration(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
bottom: BorderSide(color: colors.outline, width: 0.5),
),
),
child: Row(
children: [
// 类型选择
_buildTypeSelector(),
_buildTypeSelector(colors),
const SizedBox(width: 16),
// 评论人
Expanded(
child: TextField(
controller: _reviewerController,
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
decoration: const InputDecoration(
style: TextStyle(fontSize: 14, color: colors.onSurface),
decoration: InputDecoration(
hintText: '评论人',
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
@@ -104,10 +105,10 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
width: 100,
child: TextField(
controller: _sourceController,
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
decoration: const InputDecoration(
style: TextStyle(fontSize: 14, color: colors.onSurface),
decoration: InputDecoration(
hintText: '来源',
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
@@ -125,19 +126,19 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.7,
),
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: '写下你的书评...',
hintStyle: TextStyle(
fontSize: 16,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
),
border: InputBorder.none,
contentPadding: EdgeInsets.all(16),
contentPadding: const EdgeInsets.all(16),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
@@ -154,29 +155,29 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
}
/// 构建类型选择器
Widget _buildTypeSelector() {
Widget _buildTypeSelector(ColorScheme colors) {
return GestureDetector(
onTap: () => _showTypeSelector(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)),
border: Border.all(color: colors.outline),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_reviewType == 1 ? '短评' : '长评',
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(width: 4),
const Icon(
Icon(
Icons.arrow_drop_down,
size: 16,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
],
),
@@ -188,36 +189,42 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
void _showTypeSelector() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('短评'),
trailing: _reviewType == 1
? const Icon(Icons.check, color: Color(0xFF1A1A1A))
: null,
onTap: () {
setState(() => _reviewType = 1);
Navigator.pop(context);
},
builder: (context) {
final colors = Theme.of(context).colorScheme;
return Container(
color: colors.surface,
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('短评'),
trailing: _reviewType == 1
? Icon(Icons.check, color: colors.onSurface)
: null,
onTap: () {
setState(() => _reviewType = 1);
Navigator.pop(context);
},
),
Divider(height: 0.5, color: colors.outline),
ListTile(
title: const Text('长评'),
trailing: _reviewType == 2
? Icon(Icons.check, color: colors.onSurface)
: null,
onTap: () {
setState(() => _reviewType = 2);
Navigator.pop(context);
},
),
],
),
const Divider(height: 0.5),
ListTile(
title: const Text('长评'),
trailing: _reviewType == 2
? const Icon(Icons.check, color: Color(0xFF1A1A1A))
: null,
onTap: () {
setState(() => _reviewType = 2);
Navigator.pop(context);
},
),
],
),
),
),
);
},
);
}
@@ -259,4 +266,3 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
Navigator.pop(context);
}
}

View File

@@ -73,19 +73,20 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: _isSearching
? TextField(
controller: _searchController,
autofocus: true,
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: '搜索书评内容、书评人、来源...',
hintStyle: TextStyle(color: Color(0xFF999999)),
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)),
border: InputBorder.none,
),
style: const TextStyle(color: Color(0xFF1A1A1A)),
style: TextStyle(color: colors.onSurface),
onChanged: _onSearchChanged,
)
: const Text('书评'),
@@ -112,6 +113,7 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
}
Widget _buildEmptyState() {
final colors = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -120,21 +122,21 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
child: Icon(
Icons.rate_review_outlined,
size: 40,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
),
),
const SizedBox(height: 20),
const Text(
Text(
'暂无书评',
style: TextStyle(
fontSize: 16,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(height: 24),
@@ -143,15 +145,15 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
child: Text(
'添加记录',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
color: colors.onPrimary,
),
),
),
@@ -176,13 +178,14 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
}
Widget _buildReviewCard(BookReview review) {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: () => _navigateToReviewDetail(review),
onLongPress: () => _showDeleteDialog(review),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Column(
@@ -193,8 +196,8 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: review.reviewType == 1
? Colors.white
: const Color(0xFF1A1A1A),
? colors.surface
: colors.primary,
borderRadius: BorderRadius.circular(4),
),
child: Text(
@@ -202,8 +205,8 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
style: TextStyle(
fontSize: 10,
color: review.reviewType == 1
? const Color(0xFF666666)
: Colors.white,
? colors.onSurface.withValues(alpha: 0.6)
: colors.onPrimary,
),
),
),
@@ -215,9 +218,9 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
review.content,
maxLines: review.reviewType == 1 ? 4 : 8,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.5,
),
),
@@ -232,9 +235,9 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
Expanded(
child: Text(
review.reviewer,
style: const TextStyle(
style: TextStyle(
fontSize: 11,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
overflow: TextOverflow.ellipsis,
),
@@ -252,9 +255,9 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
Expanded(
child: Text(
review.source,
style: const TextStyle(
style: TextStyle(
fontSize: 10,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
overflow: TextOverflow.ellipsis,
),
@@ -262,9 +265,9 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
// 日期
Text(
_formatDate(review.createdAt),
style: const TextStyle(
style: TextStyle(
fontSize: 10,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
@@ -315,28 +318,31 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
void _showDeleteDialog(BookReview review) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'),
content: const Text('确定要删除这条书评吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeBookReview(review.id);
Navigator.pop(context);
_loadReviews();
ToastUtil.show(context, '已删除');
},
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),
builder: (context) {
final colors = Theme.of(context).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'),
content: const Text('确定要删除这条书评吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeBookReview(review.id);
Navigator.pop(context);
_loadReviews();
ToastUtil.show(context, '删除');
},
child: Text('删除', style: TextStyle(color: colors.error)),
),
],
);
},
);
}
}

View File

@@ -23,21 +23,22 @@ class _BookSharePageState extends State<BookSharePage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
backgroundColor: colors.surfaceContainerHighest,
appBar: AppBar(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)),
icon: Icon(Icons.close, color: colors.onSurface),
onPressed: () => Navigator.pop(context),
),
title: const Text(
title: Text(
'分享海报',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
centerTitle: true,
@@ -50,12 +51,12 @@ class _BookSharePageState extends State<BookSharePage> {
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text(
: Text(
'分享',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
),
@@ -76,13 +77,14 @@ class _BookSharePageState extends State<BookSharePage> {
/// 构建海报 Widget
Widget _buildPosterWidget() {
final colors = Theme.of(context).colorScheme;
final book = widget.book;
final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty;
return Container(
width: 320,
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
@@ -117,10 +119,10 @@ class _BookSharePageState extends State<BookSharePage> {
// 书名
Text(
book.title,
style: const TextStyle(
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
@@ -129,9 +131,9 @@ class _BookSharePageState extends State<BookSharePage> {
const SizedBox(height: 8),
Text(
book.alternateTitles.join(' / '),
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
],
@@ -157,11 +159,11 @@ class _BookSharePageState extends State<BookSharePage> {
),
),
const SizedBox(width: 4),
const Text(
Text(
'/ 10',
style: TextStyle(
fontSize: 12,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
@@ -171,44 +173,45 @@ class _BookSharePageState extends State<BookSharePage> {
// 作者
if (book.authors.isNotEmpty)
_buildInfoRow('作者', book.authors.join(' / ')),
_buildInfoRow('作者', book.authors.join(' / '), colors),
// 出版社
if (book.publisher != null && book.publisher!.isNotEmpty)
_buildInfoRow('出版社', book.publisher!),
_buildInfoRow('出版社', book.publisher!, colors),
// 出版时间
if (book.publishDate != null)
_buildInfoRow(
'出版',
'${book.publishDate!.year}.${book.publishDate!.month.toString().padLeft(2, '0')}.${book.publishDate!.day.toString().padLeft(2, '0')}',
colors,
),
// 类型
if (book.genres.isNotEmpty)
_buildInfoRow('类型', book.genres.join(' / ')),
_buildInfoRow('类型', book.genres.join(' / '), colors),
// ISBN
if (book.isbn != null && book.isbn!.isNotEmpty)
_buildInfoRow('ISBN', book.isbn!),
_buildInfoRow('ISBN', book.isbn!, colors),
const SizedBox(height: 16),
// 简介
if (book.summary != null && book.summary!.isNotEmpty) ...[
const Text(
Text(
'简介',
style: TextStyle(
fontSize: 12,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(height: 8),
Text(
book.summary!,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.6,
),
maxLines: 5,
@@ -219,7 +222,7 @@ class _BookSharePageState extends State<BookSharePage> {
const SizedBox(height: 20),
// 底部标识
const Divider(height: 1, color: Color(0xFFE8E8E8)),
Divider(height: 1, color: colors.outline),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -227,14 +230,14 @@ class _BookSharePageState extends State<BookSharePage> {
Icon(
Icons.book_outlined,
size: 14,
color: const Color(0xFF1A1A1A).withOpacity(0.5),
color: colors.onSurface.withValues(alpha: 0.5),
),
const SizedBox(width: 6),
Text(
'来自 MookNote',
style: TextStyle(
fontSize: 12,
color: const Color(0xFF1A1A1A).withOpacity(0.5),
color: colors.onSurface.withValues(alpha: 0.5),
),
),
],
@@ -248,7 +251,7 @@ class _BookSharePageState extends State<BookSharePage> {
}
/// 构建信息行
Widget _buildInfoRow(String label, String value) {
Widget _buildInfoRow(String label, String value, ColorScheme colors) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
@@ -256,17 +259,17 @@ class _BookSharePageState extends State<BookSharePage> {
children: [
Text(
'$label',
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF333333),
color: colors.onSurface.withValues(alpha: 0.75),
),
),
),

View File

@@ -17,7 +17,7 @@ class BookTabPage extends StatefulWidget {
}
class _BookTabPageState extends State<BookTabPage> {
int _layoutStyle = 0; // 0: 封面网格, 1: 列表
int _layoutStyle = 0;
bool _firstLoad = true;
@override
@@ -42,6 +42,7 @@ class _BookTabPageState extends State<BookTabPage> {
}
Widget _buildBookList(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Consumer<AppProvider>(
builder: (context, provider, child) {
final statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'};
@@ -55,8 +56,8 @@ class _BookTabPageState extends State<BookTabPage> {
if (books.isEmpty) {
return RefreshIndicator(
onRefresh: () async => await provider.loadBooks(),
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
color: colors.primary,
backgroundColor: colors.surface,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [_buildEmptyState(context, provider.bookStatusIndex)],
@@ -73,10 +74,11 @@ class _BookTabPageState extends State<BookTabPage> {
}
Widget _buildGridView(List books, AppProvider provider) {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator(
onRefresh: () async => await provider.loadBooks(),
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
color: colors.primary,
backgroundColor: colors.surface,
child: GridView.builder(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 100),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
@@ -92,10 +94,11 @@ class _BookTabPageState extends State<BookTabPage> {
}
Widget _buildListView(List books, AppProvider provider) {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator(
onRefresh: () async => await provider.loadBooks(),
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
color: colors.primary,
backgroundColor: colors.surface,
child: ListView.builder(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
itemCount: books.length,
@@ -105,6 +108,7 @@ class _BookTabPageState extends State<BookTabPage> {
}
Widget _buildListCard(book) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: () => Navigator.pushNamed(context, '/book-detail', arguments: book),
onLongPress: () => _showDeleteDialog(context, book),
@@ -112,23 +116,22 @@ class _BookTabPageState extends State<BookTabPage> {
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
// 封面缩略图
Container(
width: 48, height: 64,
decoration: BoxDecoration(
color: const Color(0xFFF0F0F0),
color: colors.outlineVariant,
borderRadius: BorderRadius.circular(6),
),
clipBehavior: Clip.antiAlias,
child: book.coverPath != null && book.coverPath!.isNotEmpty
? Image.file(File(book.coverPath!), fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Icon(Icons.menu_book_outlined, size: 22, color: Color(0xFFCCCCCC)))
: const Icon(Icons.menu_book_outlined, size: 22, color: Color(0xFFCCCCCC)),
errorBuilder: (_, __, ___) => Icon(Icons.menu_book_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)))
: Icon(Icons.menu_book_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)),
),
const SizedBox(width: 12),
Expanded(
@@ -136,11 +139,11 @@ class _BookTabPageState extends State<BookTabPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(book.title, maxLines: 1, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
if (book.authors.isNotEmpty) ...[
const SizedBox(height: 3),
Text(book.authors.take(2).join(''), maxLines: 1, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
],
const SizedBox(height: 6),
if (book.rating != null)
@@ -151,7 +154,7 @@ class _BookTabPageState extends State<BookTabPage> {
),
),
const SizedBox(width: 8),
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
],
),
),
@@ -159,19 +162,20 @@ class _BookTabPageState extends State<BookTabPage> {
}
void _showDeleteDialog(BuildContext context, book) {
final colors = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
content: Text('确定要删除《${book.title}》吗?删除后可在回收站恢复。',
style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)),
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
),
ElevatedButton(
onPressed: () async {
@@ -179,7 +183,7 @@ class _BookTabPageState extends State<BookTabPage> {
Navigator.pop(ctx);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, foregroundColor: Colors.white, elevation: 0,
backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
@@ -193,11 +197,12 @@ class _BookTabPageState extends State<BookTabPage> {
Widget _buildSkeleton() {
return _layoutStyle == 1
? MovieSkeletonGrid() // reuse same grid skeleton pattern
? MovieSkeletonGrid()
: const BookSkeletonGrid();
}
Widget _buildEmptyState(BuildContext context, int statusIndex) {
final colors = Theme.of(context).colorScheme;
final statusText = ['已读', '在读', '想读'][statusIndex];
return Center(
child: Column(
@@ -205,11 +210,11 @@ class _BookTabPageState extends State<BookTabPage> {
children: [
Container(
width: 80, height: 80,
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(20)),
child: const Icon(Icons.menu_book_outlined, size: 40, color: Color(0xFFCCCCCC)),
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)),
child: Icon(Icons.menu_book_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
),
const SizedBox(height: 20),
Text('暂无$statusText的书籍', style: const TextStyle(fontSize: 16, color: Color(0xFF999999))),
Text('暂无$statusText的书籍', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 24),
InkWell(
onTap: () {
@@ -218,8 +223,8 @@ class _BookTabPageState extends State<BookTabPage> {
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(8)),
child: const Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(8)),
child: Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)),
),
),
],

View File

@@ -99,7 +99,7 @@ class _HomePageState extends State<HomePage> {
width: 44,
height: 56,
decoration: BoxDecoration(
color: Colors.white,
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.horizontal(
right: Radius.circular(28)),
boxShadow: [
@@ -111,10 +111,10 @@ class _HomePageState extends State<HomePage> {
),
],
),
child: const Center(
child: Center(
child: Icon(
Icons.chevron_right,
color: Color(0xFF999999),
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4),
size: 24,
),
),

View File

@@ -31,7 +31,6 @@ class _MainContentPageState extends State<MainContentPage> {
_loadTabSettings();
}
/// 加载标签显示设置
void _loadTabSettings() {
setState(() {
_showMovieTab = _userPrefs.showMovieTab;
@@ -43,11 +42,9 @@ class _MainContentPageState extends State<MainContentPage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 当页面重新获得焦点时刷新设置
_loadTabSettings();
}
/// 获取启用的标签列表
List<_TabItem> get _enabledTabs {
final tabs = <_TabItem>[];
if (_showMovieTab) tabs.add(_TabItem('影视', 0));
@@ -56,7 +53,6 @@ class _MainContentPageState extends State<MainContentPage> {
return tabs;
}
/// 将原始索引映射到启用标签的索引
int _mapToEnabledTabIndex(int originalIndex) {
final tabs = _enabledTabs;
for (int i = 0; i < tabs.length; i++) {
@@ -69,13 +65,8 @@ class _MainContentPageState extends State<MainContentPage> {
Widget build(BuildContext context) {
return Column(
children: [
// 顶部 AppBar
_buildAppBar(context),
// 三个标签页的标题栏
_buildTabBar(context),
// 标签页内容
Expanded(
child: _buildTabContent(),
),
@@ -83,16 +74,13 @@ class _MainContentPageState extends State<MainContentPage> {
);
}
/// 构建顶部 AppBar
Widget _buildAppBar(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
return AppBar(
title: Text(_getAppBarTitle(provider)),
actions: [
// 云备份按钮
_buildCloudSyncButton(context),
// 搜索按钮
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
@@ -110,8 +98,8 @@ class _MainContentPageState extends State<MainContentPage> {
);
}
/// 构建云备份按钮
Widget _buildCloudSyncButton(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return PopupMenuButton<String>(
icon: const Icon(Icons.cloud_sync_outlined),
tooltip: '云备份',
@@ -128,21 +116,21 @@ class _MainContentPageState extends State<MainContentPage> {
width: 32,
height: 32,
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
child: Icon(
Icons.cloud_upload_outlined,
size: 18,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(width: 12),
const Text(
Text(
'上传数据',
style: TextStyle(
fontSize: 14,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
],
@@ -156,21 +144,21 @@ class _MainContentPageState extends State<MainContentPage> {
width: 32,
height: 32,
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
child: Icon(
Icons.cloud_download_outlined,
size: 18,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(width: 12),
const Text(
Text(
'下载数据',
style: TextStyle(
fontSize: 14,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
],
@@ -185,21 +173,21 @@ class _MainContentPageState extends State<MainContentPage> {
width: 32,
height: 32,
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
child: Icon(
Icons.settings_outlined,
size: 18,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(width: 12),
const Text(
Text(
'WebDAV设置',
style: TextStyle(
fontSize: 14,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
],
@@ -227,9 +215,8 @@ class _MainContentPageState extends State<MainContentPage> {
);
}
/// 执行同步操作
Future<void> _performSync(BuildContext context, SyncDirection direction) async {
// 检查是否已配置
final colors = Theme.of(context).colorScheme;
final config = await WebDAVService.instance.getConfig();
if (config == null) {
if (context.mounted) {
@@ -243,28 +230,24 @@ class _MainContentPageState extends State<MainContentPage> {
return;
}
// 显示加载对话框
if (context.mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
builder: (context) => Center(
child: CircularProgressIndicator(
color: Color(0xFF1A1A1A),
color: colors.primary,
),
),
);
}
// 执行同步
final result = await WebDAVService.instance.syncData(direction: direction);
// 关闭加载对话框
if (context.mounted) {
Navigator.pop(context);
}
// 下载了数据则刷新界面
if (result.success && result.needReload && context.mounted) {
final provider = context.read<AppProvider>();
await provider.loadMovies();
@@ -272,11 +255,10 @@ class _MainContentPageState extends State<MainContentPage> {
await provider.loadNotes();
}
// 显示结果
if (context.mounted) {
final isSuccess = result.success;
final message = result.message.isNotEmpty ? result.message : (isSuccess ? '同步成功' : '同步失败');
_showResultDialog(
context,
title: isSuccess ? '同步成功' : '同步失败',
@@ -290,7 +272,6 @@ class _MainContentPageState extends State<MainContentPage> {
}
}
/// 显示同步结果对话框
void _showResultDialog(
BuildContext context, {
required String title,
@@ -298,10 +279,11 @@ class _MainContentPageState extends State<MainContentPage> {
required bool isSuccess,
Map<String, dynamic>? details,
}) {
final colors = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: Row(
@@ -322,9 +304,10 @@ class _MainContentPageState extends State<MainContentPage> {
const SizedBox(width: 12),
Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
],
@@ -335,9 +318,9 @@ class _MainContentPageState extends State<MainContentPage> {
children: [
Text(
message,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.5,
),
),
@@ -346,7 +329,7 @@ class _MainContentPageState extends State<MainContentPage> {
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
),
child: Column(
@@ -370,8 +353,8 @@ class _MainContentPageState extends State<MainContentPage> {
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1A1A1A),
foregroundColor: Colors.white,
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
@@ -386,8 +369,8 @@ class _MainContentPageState extends State<MainContentPage> {
);
}
/// 构建详情行
Widget _buildDetailRow(String label, String value, {bool isError = false}) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
@@ -395,9 +378,9 @@ class _MainContentPageState extends State<MainContentPage> {
children: [
Text(
label,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
Text(
@@ -405,7 +388,7 @@ class _MainContentPageState extends State<MainContentPage> {
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: isError ? const Color(0xFFE57373) : const Color(0xFF1A1A1A),
color: isError ? const Color(0xFFE57373) : colors.onSurface,
),
),
],
@@ -413,7 +396,6 @@ class _MainContentPageState extends State<MainContentPage> {
);
}
/// 获取 AppBar 标题
String _getAppBarTitle(AppProvider provider) {
switch (provider.mainTabIndex) {
case 0:
@@ -440,17 +422,17 @@ class _MainContentPageState extends State<MainContentPage> {
}
}
/// 构建标签栏
Widget _buildTabBar(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
final colors = Theme.of(context).colorScheme;
final tabs = _enabledTabs;
final currentEnabledIndex = _mapToEnabledTabIndex(provider.mainTabIndex);
final safeIndex = currentEnabledIndex < tabs.length ? currentEnabledIndex : 0;
return Container(
decoration: const BoxDecoration(
color: Colors.white,
decoration: BoxDecoration(
color: colors.surface,
),
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -474,7 +456,7 @@ class _MainContentPageState extends State<MainContentPage> {
Icon(
_getTabIcon(tab.label),
size: 22,
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFB0B0B0),
color: isSelected ? colors.primary : colors.onSurface.withValues(alpha: 0.35),
),
const SizedBox(height: 6),
Text(
@@ -482,7 +464,7 @@ class _MainContentPageState extends State<MainContentPage> {
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFB0B0B0),
color: isSelected ? colors.primary : colors.onSurface.withValues(alpha: 0.35),
),
),
],
@@ -513,7 +495,7 @@ class _MainContentPageState extends State<MainContentPage> {
width: indicatorWidth,
height: 3,
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(1.5),
),
),
@@ -524,7 +506,7 @@ class _MainContentPageState extends State<MainContentPage> {
},
),
),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
],
),
);
@@ -532,12 +514,10 @@ class _MainContentPageState extends State<MainContentPage> {
);
}
/// 构建标签页内容
Widget _buildTabContent() {
return Consumer<AppProvider>(
builder: (context, provider, child) {
final tabs = _enabledTabs;
// 找到当前应该显示的标签
_TabItem? currentTab;
for (final tab in tabs) {
if (tab.originalIndex == provider.mainTabIndex) {
@@ -545,10 +525,8 @@ class _MainContentPageState extends State<MainContentPage> {
break;
}
}
// 如果当前标签被禁用了,显示第一个启用的标签
if (currentTab == null && tabs.isNotEmpty) {
currentTab = tabs.first;
// 更新 provider 的索引
WidgetsBinding.instance.addPostFrameCallback((_) {
provider.setMainTabIndex(currentTab!.originalIndex);
});
@@ -572,22 +550,21 @@ class _MainContentPageState extends State<MainContentPage> {
);
}
/// 显示添加对话框
void _showAddDialog(BuildContext context, AppProvider provider) {
final colors = Theme.of(context).colorScheme;
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
backgroundColor: colors.surface,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
builder: (BuildContext context) {
return SafeArea(
child: Wrap(
children: [
ListTile(
leading: const Icon(Icons.movie, color: Color(0xFF1A1A1A)),
leading: Icon(Icons.movie, color: colors.onSurface),
title: const Text('添加观影'),
onTap: () {
Navigator.pop(context);
// 根据当前影视标签页的选中状态设置默认值
final statusMap = {
0: 'watched',
1: 'watching',
@@ -601,13 +578,12 @@ class _MainContentPageState extends State<MainContentPage> {
);
},
),
const Divider(height: 0.5, indent: 56),
Divider(height: 0.5, indent: 56, color: colors.outlineVariant),
ListTile(
leading: const Icon(Icons.menu_book, color: Color(0xFF1A1A1A)),
leading: Icon(Icons.menu_book, color: colors.onSurface),
title: const Text('添加阅读'),
onTap: () {
Navigator.pop(context);
// 根据当前阅读标签页的选中状态设置默认值
final statusMap = {
0: 'read',
1: 'reading',
@@ -621,9 +597,9 @@ class _MainContentPageState extends State<MainContentPage> {
);
},
),
const Divider(height: 0.5, indent: 56),
Divider(height: 0.5, indent: 56, color: colors.outlineVariant),
ListTile(
leading: const Icon(Icons.note, color: Color(0xFF1A1A1A)),
leading: Icon(Icons.note, color: colors.onSurface),
title: const Text('添加笔记'),
onTap: () {
Navigator.pop(context);
@@ -638,7 +614,6 @@ class _MainContentPageState extends State<MainContentPage> {
}
}
/// 标签项数据类
class _TabItem {
final String label;
final int originalIndex;

View File

@@ -90,26 +90,30 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
void _showPermissionDeniedDialog() {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('需要存储权限', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
content: const Text(
'Android 11+ 需要在系统设置中授予"所有文件访问权限"才能读取目录中的 Markdown 文件。\n\n是否前往设置?',
style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('取消', style: TextStyle(color: Color(0xFF999999))),
builder: (ctx) {
final colors = Theme.of(ctx).colorScheme;
return AlertDialog(
title: const Text('需要存储权限',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
content: Text(
'Android 11+ 需要在系统设置中授予"所有文件访问权限"才能读取目录中的 Markdown 文件。\n\n是否前往设置?',
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6),
),
TextButton(
onPressed: () {
Navigator.pop(ctx);
openAppSettings();
},
child: const Text('前往设置', style: TextStyle(color: Color(0xFF1A1A1A))),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4))),
),
TextButton(
onPressed: () {
Navigator.pop(ctx);
openAppSettings();
},
child: Text('前往设置', style: TextStyle(color: colors.onSurface)),
),
],
);
},
);
}
@@ -121,7 +125,10 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
for (final entity in dir.listSync(recursive: true, followLinks: false)) {
if (entity is File) {
final lower = p.basename(entity.path).toLowerCase();
if (lower.endsWith('.md') || lower.endsWith('.markdown') || lower.endsWith('.mdown') || lower.endsWith('.txt')) {
if (lower.endsWith('.md') ||
lower.endsWith('.markdown') ||
lower.endsWith('.mdown') ||
lower.endsWith('.txt')) {
return true;
}
}
@@ -135,13 +142,28 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
try {
final dir = Directory(dirPath);
if (!dir.existsSync()) return false;
const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico', '.tiff', '.heic', '.webm'];
const imageExts = [
'.png',
'.jpg',
'.jpeg',
'.gif',
'.webp',
'.bmp',
'.svg',
'.ico',
'.tiff',
'.heic',
'.webm'
];
bool hasAnyFile = false;
for (final entity in dir.listSync(recursive: true, followLinks: false)) {
if (entity is File) {
hasAnyFile = true;
final lower = p.basename(entity.path).toLowerCase();
if (lower.endsWith('.md') || lower.endsWith('.markdown') || lower.endsWith('.mdown') || lower.endsWith('.txt')) {
if (lower.endsWith('.md') ||
lower.endsWith('.markdown') ||
lower.endsWith('.mdown') ||
lower.endsWith('.txt')) {
return false;
}
if (!imageExts.any((ext) => lower.endsWith(ext))) {
@@ -193,9 +215,13 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
}
} else if (entity is File) {
final lower = name.toLowerCase();
if (lower.endsWith('.md') || lower.endsWith('.markdown') || lower.endsWith('.mdown') || lower.endsWith('.txt')) {
if (lower.endsWith('.md') ||
lower.endsWith('.markdown') ||
lower.endsWith('.mdown') ||
lower.endsWith('.txt')) {
final stat = entity.statSync();
entries.add(_FileEntry(name: name, path: entity.path, isDir: false, size: stat.size));
entries.add(_FileEntry(
name: name, path: entity.path, isDir: false, size: stat.size));
}
}
}
@@ -236,9 +262,10 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
}
void _openFile(_FileEntry entry) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => MdViewerPage(filePath: entry.path),
));
Navigator.push(context,
MaterialPageRoute(
builder: (context) => MdViewerPage(filePath: entry.path),
));
}
void _showSettingsSheet() {
@@ -249,6 +276,7 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (ctx) {
final colors = Theme.of(ctx).colorScheme;
return StatefulBuilder(
builder: (ctx, setLocalState) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
@@ -256,24 +284,31 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 36, height: 4,
width: 36,
height: 4,
decoration: BoxDecoration(
color: const Color(0xFFDDDDDD),
color: colors.onSurface.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
const Align(
Align(
alignment: Alignment.centerLeft,
child: Text('目录显示设置', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
child: Text('目录显示设置',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
),
const SizedBox(height: 16),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('显示空目录', style: TextStyle(fontSize: 14, color: Color(0xFF333333))),
subtitle: const Text('关闭后隐藏无 Markdown 文件的目录', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
title: Text('显示空目录',
style: TextStyle(
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75))),
subtitle: Text('关闭后隐藏无 Markdown 文件的目录',
style:
TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
value: _showEmptyDirs,
activeColor: const Color(0xFF1A1A1A),
activeTrackColor: colors.primary,
onChanged: (val) {
setLocalState(() => _showEmptyDirs = val);
UserPrefs().setShowEmptyDirs(val);
@@ -281,13 +316,17 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
_loadDirectory();
},
),
const Divider(height: 0.5, color: Color(0xFFF0F0F0)),
Divider(height: 0.5, color: colors.outlineVariant),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('显示纯图片目录', style: TextStyle(fontSize: 14, color: Color(0xFF333333))),
subtitle: const Text('关闭后隐藏只含图片、无 Markdown 的目录', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
title: Text('显示纯图片目录',
style: TextStyle(
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75))),
subtitle: Text('关闭后隐藏只含图片、无 Markdown 的目录',
style:
TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
value: _showImageOnlyDirs,
activeColor: const Color(0xFF1A1A1A),
activeTrackColor: colors.primary,
onChanged: (val) {
setLocalState(() => _showImageOnlyDirs = val);
UserPrefs().setShowImageOnlyDirs(val);
@@ -304,40 +343,26 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
);
}
bool get _canGoBack => _currentPath != null && _rootPath != null && _currentPath != _rootPath;
bool get _canGoBack =>
_currentPath != null && _rootPath != null && _currentPath != _rootPath;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
title: Text(
_currentPath != null ? p.basename(_currentPath!) : 'Markdown 阅读',
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFF1A1A1A)),
icon: Icon(Icons.arrow_back, color: colors.onSurface),
onPressed: () => Navigator.pop(context),
),
actions: [
// if (_canGoBack)
// Padding(
// padding: const EdgeInsets.only(right: 8),
// child: GestureDetector(
// onTap: _goBack,
// child: Container(
// padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
// decoration: BoxDecoration(
// color: const Color(0xFFF5F5F5),
// borderRadius: BorderRadius.circular(14),
// border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
// ),
// child: const Text('返回上级', style: TextStyle(fontSize: 12, color: Color(0xFF666666))),
// ),
// ),
// ),
if (_currentPath != null)
Padding(
padding: const EdgeInsets.only(right: 4),
@@ -346,86 +371,109 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outline, width: 0.5),
),
child: const Text('更换目录', style: TextStyle(fontSize: 12, color: Color(0xFF888888))),
child: Text('更换目录',
style: TextStyle(
fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5))),
),
),
),
IconButton(
icon: const Icon(Icons.tune, size: 20, color: Color(0xFF888888)),
icon: Icon(Icons.tune, size: 20, color: colors.onSurface.withValues(alpha: 0.5)),
onPressed: _showSettingsSheet,
),
const SizedBox(width: 4),
],
),
body: _buildBody(),
body: _buildBody(colors),
);
}
Widget _buildBody() {
Widget _buildBody(ColorScheme colors) {
if (_currentPath == null) {
return _buildWelcome();
return _buildWelcome(colors);
}
if (_isLoading) {
return const Center(child: CircularProgressIndicator(color: Color(0xFF1A1A1A)));
return Center(child: CircularProgressIndicator(color: colors.primary));
}
if (_error != null) {
return _buildError();
return _buildError(colors);
}
return RefreshIndicator(
onRefresh: _loadDirectory,
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
child: _entries.isEmpty ? _buildEmpty() : ListView.separated(
padding: EdgeInsets.zero,
itemCount: (_canGoBack ? 1 : 0) + _entries.length,
separatorBuilder: (_, __) => const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFF0F0F0)),
itemBuilder: (context, index) {
if (_canGoBack && index == 0) {
return ListTile(
leading: Container(
width: 36, height: 36,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.arrow_upward, size: 18, color: Color(0xFF888888)),
),
title: const Text('..', style: TextStyle(fontSize: 14, color: Color(0xFF888888))),
onTap: _goBack,
);
}
final entry = _entries[_canGoBack ? index - 1 : index];
return ListTile(
leading: Container(
width: 36, height: 36,
decoration: BoxDecoration(
color: entry.isDir ? const Color(0xFFF0F7FF) : const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
entry.isDir ? Icons.folder_outlined : Icons.description_outlined,
size: 18,
color: entry.isDir ? const Color(0xFF4A90D9) : const Color(0xFF666666),
),
color: colors.primary,
backgroundColor: colors.surface,
child: _entries.isEmpty
? _buildEmpty(colors)
: ListView.separated(
padding: EdgeInsets.zero,
itemCount: (_canGoBack ? 1 : 0) + _entries.length,
separatorBuilder: (_, __) =>
Divider(height: 0.5, thickness: 0.5, color: colors.outlineVariant),
itemBuilder: (context, index) {
if (_canGoBack && index == 0) {
return ListTile(
leading: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.arrow_upward,
size: 18, color: colors.onSurface.withValues(alpha: 0.5)),
),
title: Text('..',
style: TextStyle(
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.5))),
onTap: _goBack,
);
}
final entry = _entries[_canGoBack ? index - 1 : index];
return ListTile(
leading: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: entry.isDir
? const Color(0xFFF0F7FF)
: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
entry.isDir ? Icons.folder_outlined : Icons.description_outlined,
size: 18,
color: entry.isDir
? const Color(0xFF4A90D9)
: colors.onSurface.withValues(alpha: 0.6),
),
),
title: Text(entry.name,
style: TextStyle(fontSize: 14, color: colors.onSurface),
maxLines: 1,
overflow: TextOverflow.ellipsis),
subtitle: entry.isDir
? null
: Text(_formatSize(entry.size),
style: TextStyle(
fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
trailing: Icon(entry.isDir ? Icons.chevron_right : Icons.open_in_new_outlined,
size: 16, color: colors.onSurface.withValues(alpha: 0.25)),
onTap: () =>
entry.isDir ? _enterDirectory(entry.path) : _openFile(entry),
);
},
),
title: Text(entry.name, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)), maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: entry.isDir ? null : Text(_formatSize(entry.size), style: const TextStyle(fontSize: 11, color: Color(0xFF999999))),
trailing: Icon(entry.isDir ? Icons.chevron_right : Icons.open_in_new_outlined, size: 16, color: const Color(0xFFCCCCCC)),
onTap: () => entry.isDir ? _enterDirectory(entry.path) : _openFile(entry),
);
},
),
);
}
Widget _buildWelcome() {
Widget _buildWelcome(ColorScheme colors) {
return Center(
child: Padding(
padding: const EdgeInsets.all(40),
@@ -433,27 +481,34 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 80, height: 80,
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(Icons.folder_open_outlined, size: 40, color: Color(0xFFCCCCCC)),
child: Icon(Icons.folder_open_outlined,
size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
),
const SizedBox(height: 24),
const Text('Markdown 阅读', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
Text('Markdown 阅读',
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.w600, color: colors.onSurface)),
const SizedBox(height: 8),
const Text('选择一个包含 .md 文件的文件夹', style: TextStyle(fontSize: 14, color: Color(0xFF999999))),
Text('选择一个包含 .md 文件的文件夹',
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 32),
GestureDetector(
onTap: _pickDirectory,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(24),
),
child: const Text('选择目录', style: TextStyle(fontSize: 15, color: Colors.white, fontWeight: FontWeight.w500)),
child: Text('选择目录',
style: TextStyle(
fontSize: 15, color: colors.onPrimary, fontWeight: FontWeight.w500)),
),
),
],
@@ -462,28 +517,31 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
);
}
Widget _buildEmpty() {
Widget _buildEmpty(ColorScheme colors) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.folder_open_outlined, size: 64, color: Color(0xFFE0E0E0)),
Icon(Icons.folder_open_outlined, size: 64, color: colors.outline),
const SizedBox(height: 16),
const Text('此目录下没有 Markdown 文件', style: TextStyle(fontSize: 15, color: Color(0xFF999999))),
Text('此目录下没有 Markdown 文件',
style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 4),
Text(_currentPath ?? '', style: const TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))),
Text(_currentPath ?? '',
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.25))),
const SizedBox(height: 24),
GestureDetector(
onTap: _canGoBack ? _goBack : () => _pickDirectory(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFDDDDDD)),
border: Border.all(color: colors.onSurface.withValues(alpha: 0.15)),
borderRadius: BorderRadius.circular(20),
),
child: Text(
_canGoBack ? '返回上级目录' : '换一个目录',
style: const TextStyle(fontSize: 13, color: Color(0xFF888888)),
style: TextStyle(
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5)),
),
),
),
@@ -492,18 +550,22 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
);
}
Widget _buildError() {
Widget _buildError(ColorScheme colors) {
return Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48, color: Color(0xFFCCCCCC)),
Icon(Icons.error_outline,
size: 48, color: colors.onSurface.withValues(alpha: 0.25)),
const SizedBox(height: 16),
Text(_error!, style: const TextStyle(fontSize: 14, color: Color(0xFF999999)), textAlign: TextAlign.center),
Text(_error!,
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4)),
textAlign: TextAlign.center),
const SizedBox(height: 8),
Text('路径: ${_currentPath ?? ""}', style: const TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))),
Text('路径: ${_currentPath ?? ""}',
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.25))),
const SizedBox(height: 24),
Row(
mainAxisSize: MainAxisSize.min,
@@ -513,10 +575,11 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(20),
),
child: const Text('重试', style: TextStyle(fontSize: 13, color: Colors.white)),
child: Text('重试',
style: TextStyle(fontSize: 13, color: colors.onPrimary)),
),
),
const SizedBox(width: 12),
@@ -525,10 +588,12 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFDDDDDD)),
border: Border.all(color: colors.onSurface.withValues(alpha: 0.15)),
borderRadius: BorderRadius.circular(20),
),
child: const Text('更换目录', style: TextStyle(fontSize: 13, color: Color(0xFF888888))),
child: Text('更换目录',
style: TextStyle(
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
),
),
],

View File

@@ -50,10 +50,11 @@ class _MdViewerPageState extends State<MdViewerPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final fileName = widget.filePath.split('/').last;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
elevation: 0,
title: Text(
@@ -61,69 +62,71 @@ class _MdViewerPageState extends State<MdViewerPage> {
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
),
),
body: _buildBody(),
body: _buildBody(colors),
);
}
Widget _buildBody() {
Widget _buildBody(ColorScheme colors) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator(color: Color(0xFF1A1A1A)));
return Center(child: CircularProgressIndicator(color: colors.primary));
}
if (_error != null) {
return _buildErrorState();
return _buildErrorState(colors);
}
return Markdown(
data: _content,
padding: const EdgeInsets.all(20),
styleSheet: MarkdownStyleSheet(
h1: const TextStyle(
h1: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.4,
),
h2: const TextStyle(
h2: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.4,
),
h3: const TextStyle(
h3: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.4,
),
p: const TextStyle(
p: TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.8,
),
code: const TextStyle(
code: TextStyle(
fontSize: 13,
color: Color(0xFF1A1A1A),
backgroundColor: Color(0xFFF5F5F5),
color: colors.onSurface,
backgroundColor: colors.surfaceContainerHighest,
),
codeblockDecoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
border: Border.all(color: const Color(0xFFE5E5E5)),
color: colors.surfaceContainerHighest,
border: Border.all(color: colors.outline),
borderRadius: BorderRadius.circular(6),
),
codeblockPadding: const EdgeInsets.all(12),
blockquote: const TextStyle(
blockquote: TextStyle(
fontSize: 15,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
fontStyle: FontStyle.italic,
),
blockquoteDecoration: const BoxDecoration(
border: Border(left: BorderSide(color: Color(0xFF999999), width: 4)),
blockquoteDecoration: BoxDecoration(
border: Border(
left: BorderSide(
color: colors.onSurface.withValues(alpha: 0.4), width: 4)),
),
blockquotePadding: const EdgeInsets.only(left: 12),
listBullet: const TextStyle(
listBullet: TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
listIndent: 24,
a: const TextStyle(
@@ -133,12 +136,12 @@ class _MdViewerPageState extends State<MdViewerPage> {
),
),
// ignore: deprecated_member_use
imageBuilder: (uri, title, alt) => _buildImage(uri.toString(), alt),
imageBuilder: (uri, title, alt) => _buildImage(colors, uri.toString(), alt),
);
}
/// 构建图片显示
Widget _buildImage(String uri, String? alt) {
Widget _buildImage(ColorScheme colors, String uri, String? alt) {
if (uri.isEmpty) return const SizedBox.shrink();
// 处理相对路径:基于 md 文件所在目录
@@ -158,17 +161,19 @@ class _MdViewerPageState extends State<MdViewerPage> {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.broken_image_outlined, size: 20, color: Color(0xFF999999)),
Icon(Icons.broken_image_outlined,
size: 20, color: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 8),
Expanded(
child: Text(
alt ?? '图片加载失败',
style: const TextStyle(fontSize: 13, color: Color(0xFF999999)),
style:
TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4)),
),
),
],
@@ -179,16 +184,17 @@ class _MdViewerPageState extends State<MdViewerPage> {
);
}
Widget _buildErrorState() {
Widget _buildErrorState(ColorScheme colors) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Color(0xFFCCCCCC)),
Icon(Icons.error_outline,
size: 48, color: colors.onSurface.withValues(alpha: 0.25)),
const SizedBox(height: 16),
Text(
_error!,
style: const TextStyle(fontSize: 14, color: Color(0xFF999999)),
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4)),
),
],
),

View File

@@ -5,9 +5,9 @@ import 'package:webview_flutter/webview_flutter.dart';
/// 豆瓣影视WebView页面 - 用于抓取影视信息
class DoubanWebViewPage extends StatefulWidget {
final String url;
const DoubanWebViewPage({super.key, required this.url});
@override
State<DoubanWebViewPage> createState() => _DoubanWebViewPageState();
}
@@ -16,21 +16,21 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
late WebViewController _controller;
bool _isLoading = true;
bool _canExtract = false;
bool _isExtracting = false; // 防止重复提取
bool _isExtracting = false; // 防止重复提取
@override
void initState() {
super.initState();
_initWebView();
}
@override
void dispose() {
// 清理 WebView 资源
_controller.loadRequest(Uri.parse('about:blank'));
super.dispose();
}
void _initWebView() {
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
@@ -55,23 +55,26 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
)
..loadRequest(Uri.parse(widget.url));
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: const Text('豆瓣影视'),
leading: _buildBackButton(),
actions: [
// 提取按钮 - 始终显示
_buildActionButton(
colors: colors,
icon: Icons.auto_fix_high_outlined,
onPressed: _showExtractedInfo,
tooltip: '提取信息',
),
// 刷新按钮
_buildActionButton(
colors: colors,
icon: Icons.refresh,
onPressed: () => _controller.reload(),
tooltip: '刷新',
@@ -83,16 +86,14 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
children: [
WebViewWidget(controller: _controller),
// 加载指示器
if (_isLoading)
const Center(
child: CircularProgressIndicator(),
),
if (_isLoading) const Center(
child: CircularProgressIndicator(),
),
],
),
);
}
/// 构建返回按钮
Widget _buildBackButton() {
return Container(
@@ -121,6 +122,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
/// 构建右上角操作按钮
Widget _buildActionButton({
required ColorScheme colors,
required IconData icon,
required VoidCallback onPressed,
required String tooltip,
@@ -138,75 +140,86 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(8),
child: Icon(icon, color: const Color(0xFF1A1A1A), size: 22),
child: Icon(icon, color: colors.onSurface, size: 22),
),
),
),
);
}
/// 显示提取的信息对话框
Future<void> _showExtractedInfo() async {
// 先提取信息
final movieInfo = await _extractMovieInfo();
if (movieInfo == null) return;
// 显示提取的信息
if (mounted) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text(
'提取的影视信息',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow('标题', movieInfo['title']?.toString() ?? '未提取到'),
_buildInfoRow('导演', movieInfo['director']?.toString() ?? '未提取到'),
_buildInfoRow('类型', movieInfo['genres']?.toString() ?? '未提取到'),
_buildInfoRow('上映日期', movieInfo['releaseDate']?.toString() ?? '未提取到'),
if (movieInfo['summary'] != null)
_buildInfoRow('简介', movieInfo['summary'].toString().substring(0,
movieInfo['summary'].toString().length > 100 ? 100 : movieInfo['summary'].toString().length) + '...'),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
'取消',
style: TextStyle(color: Color(0xFF999999)),
builder: (ctx) {
final colors = Theme.of(ctx).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: Text(
'提取的影视信息',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
TextButton(
onPressed: () {
Navigator.pop(context);
Navigator.pop(context, movieInfo);
},
child: const Text(
'使用此信息',
style: TextStyle(color: Color(0xFF1A1A1A), fontWeight: FontWeight.w600),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(colors, '标题', movieInfo['title']?.toString() ?? '未提取到'),
_buildInfoRow(colors, '导演', movieInfo['director']?.toString() ?? '未提取到'),
_buildInfoRow(colors, '类型', movieInfo['genres']?.toString() ?? '未提取到'),
_buildInfoRow(colors, '上映日期', movieInfo['releaseDate']?.toString() ?? '未提取到'),
if (movieInfo['summary'] != null)
_buildInfoRow(
colors,
'简介',
movieInfo['summary'].toString().substring(
0,
movieInfo['summary'].toString().length > 100
? 100
: movieInfo['summary'].toString().length) +
'...'),
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(
'取消',
style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)),
),
),
TextButton(
onPressed: () {
Navigator.pop(ctx);
Navigator.pop(context, movieInfo);
},
child: Text(
'使用此信息',
style: TextStyle(
color: colors.onSurface, fontWeight: FontWeight.w600),
),
),
],
);
},
);
}
}
/// 构建信息行
Widget _buildInfoRow(String label, String value) {
Widget _buildInfoRow(ColorScheme colors, String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
@@ -216,18 +229,18 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
width: 64,
child: Text(
label,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
),
@@ -240,10 +253,10 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
Future<Map<String, dynamic>?> _extractMovieInfo() async {
// 检查是否已提取过,避免重复点击
if (_isExtracting) return null;
try {
_isExtracting = true;
// 显示加载提示
showDialog(
context: context,
@@ -252,16 +265,16 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
child: CircularProgressIndicator(),
),
);
// 执行JavaScript代码提取页面信息
final result = await _controller.runJavaScriptReturningResult(r'''
(function() {
const info = {};
// 获取标题 - 移动版页面
const titleEl = document.querySelector('.sub-title');
info.title = titleEl ? titleEl.textContent.trim() : '';
// 获取年份 - 从 original-title 中提取
const originalTitleEl = document.querySelector('.sub-original-title');
if (originalTitleEl) {
@@ -270,7 +283,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} else {
info.year = '';
}
// 获取封面图 - 从 sub-cover 中的 img 标签获取
const coverEl = document.querySelector('.sub-cover img');
if (coverEl) {
@@ -283,11 +296,11 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} else {
info.coverUrl = '';
}
// 获取评分 - 移动版可能在 mark-item 中
const ratingEl = document.querySelector('.rating-num') || document.querySelector('.score');
info.rating = ratingEl ? ratingEl.textContent.trim() : '';
// 获取导演 - 从演职员列表中找
const directorEl = document.querySelector('.movie-celebrities .item__celebrity .role');
if (directorEl && directorEl.textContent.includes('导演')) {
@@ -296,7 +309,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} else {
info.director = '';
}
// 获取编剧 - 从演职员列表中找(匹配"编剧"或"剧本"
const writerEls = document.querySelectorAll('.movie-celebrities .item__celebrity');
const writers = [];
@@ -308,7 +321,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
}
});
info.writers = writers;
// 获取主演- 从演职员列表中找前5个
const actorEls = document.querySelectorAll('.movie-celebrities .item__celebrity');
const actors = [];
@@ -316,10 +329,10 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
const roleEl = el
.querySelector('.role');
if (roleEl && (
roleEl.textContent.includes('配音') ||
roleEl.textContent.includes('主演') ||
roleEl.textContent.includes('配音') ||
roleEl.textContent.includes('主演') ||
roleEl.textContent.includes('演员') ||
roleEl.textContent.includes('参演') ||
roleEl.textContent.includes('参演') ||
roleEl.textContent.includes('饰')
)) {
const nameEl = el.querySelector('.name');
@@ -327,20 +340,20 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
}
});
info.actors = actors;
// 获取类型 - 从 sub-meta 或标签中提取
const metaEl = document.querySelector('.sub-meta');
if (metaEl) {
const metaText = metaEl.textContent;
const parts = metaText.split('/').map(s => s.trim());
// 过滤出类型(通常是中文,不是日期,不是时长)
info.genres = parts.filter(p =>
info.genres = parts.filter(p =>
p && !p.match(/^\d{4}/) && !p.includes('分钟') && !p.includes('上映')
).join(',');
} else {
info.genres = '';
}
// 获取上映日期
if (metaEl) {
const dateMatch = metaEl.textContent.match(/(\d{4}-\d{2}-\d{2})/);
@@ -348,7 +361,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} else {
info.releaseDate = '';
}
// 获取简介
const summaryEl = document.querySelector('.subject-intro p');
if (summaryEl) {
@@ -356,7 +369,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} else {
info.summary = '';
}
// 获取别名 - 从 original-title 中提取(去掉年份)
if (originalTitleEl) {
const fullText = originalTitleEl.textContent.trim();
@@ -364,14 +377,14 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} else {
info.alternateTitles = [];
}
return JSON.stringify(info);
})()
''');
// 关闭加载提示
if (mounted) Navigator.pop(context);
// 解析提取的信息
// result 是 JavaScript 执行结果,已经是 JSON 字符串(带引号的)
final String jsonStr = result.toString();
@@ -380,12 +393,12 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
? jsonDecode(jsonStr) as String
: jsonStr;
final Map<String, dynamic> movieInfo = jsonDecode(cleanJson) as Map<String, dynamic>;
return movieInfo;
} catch (e) {
// 关闭加载提示
if (mounted) Navigator.pop(context);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('提取信息失败: $e')),

View File

@@ -16,9 +16,9 @@ import 'movie_share_page.dart';
/// 影视详情页 - 极简主义设计
class MovieDetailPage extends StatefulWidget {
final Movie movie;
const MovieDetailPage({super.key, required this.movie});
@override
State<MovieDetailPage> createState() => _MovieDetailPageState();
}
@@ -27,77 +27,53 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 页面获得焦点时刷新数据
_refreshMovieData();
}
void _refreshMovieData() {
final provider = context.read<AppProvider>();
// 强制刷新当前影视数据
provider.loadMovies();
}
@override
Widget build(BuildContext context) {
// 从 Provider 获取最新的 movie 数据,实现动态刷新
final colors = Theme.of(context).colorScheme;
final movie = context.watch<AppProvider>().movies
.where((m) => m.id == widget.movie.id)
.firstOrNull ?? widget.movie;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
body: Stack(
children: [
CustomScrollView(
slivers: [
// 顶部海报区域
_buildSliverAppBar(movie),
// 内容区域
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 基本信息
_buildBasicInfo(movie),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 导演
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
if (movie.directors.isNotEmpty)
_buildDirectorsSection(movie),
// 编剧
if (movie.writers.isNotEmpty)
_buildWritersSection(movie),
// 主演
if (movie.actors.isNotEmpty)
_buildActorsSection(movie),
// 类型
if (movie.genres.isNotEmpty)
_buildGenresSection(movie),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 简介
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
if (movie.summary != null && movie.summary!.isNotEmpty)
_buildSummarySection(movie),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 影评和海报墙入口
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildExtraSections(movie),
const SizedBox(height: 120),
],
),
),
],
),
// 右下角悬浮按钮组
Positioned(
right: 16,
bottom: 24,
@@ -107,9 +83,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
),
);
}
/// 构建右下角悬浮按钮组
Widget _buildFloatingActionButtons(Movie movie) {
final colors = Theme.of(context).colorScheme;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -117,13 +93,16 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
icon: Icons.edit_outlined,
onPressed: () => _navigateToEdit(context),
tooltip: '编辑',
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
),
const SizedBox(height: 12),
_buildFloatingButton(
icon: Icons.delete_outline,
onPressed: () => _showDeleteDialog(context),
tooltip: '删除',
backgroundColor: Colors.red,
backgroundColor: colors.error,
foregroundColor: colors.onError,
),
const SizedBox(height: 12),
_buildFloatingButton(
@@ -131,17 +110,18 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
onPressed: () => _showSharePoster(movie),
tooltip: '分享海报',
backgroundColor: const Color(0xFF4CAF50),
foregroundColor: Colors.white,
),
],
);
}
/// 构建单个悬浮按钮
Widget _buildFloatingButton({
required IconData icon,
required VoidCallback onPressed,
required String tooltip,
Color backgroundColor = const Color(0xFF1A1A1A),
required Color backgroundColor,
required Color foregroundColor,
}) {
return Material(
color: Colors.transparent,
@@ -160,15 +140,15 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
],
),
child: IconButton(
icon: Icon(icon, size: 18, color: Colors.white),
icon: Icon(icon, size: 18, color: foregroundColor),
onPressed: onPressed,
padding: EdgeInsets.zero,
tooltip: tooltip,
),
),
);
}
/// 构建带背景的返回按钮
Widget _buildBackButton() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
@@ -194,21 +174,19 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
);
}
/// 构建顶部 AppBar
Widget _buildSliverAppBar(Movie movie) {
final colors = Theme.of(context).colorScheme;
return SliverAppBar(
expandedHeight: 320,
pinned: true,
backgroundColor: const Color(0xFFF5F5F5),
backgroundColor: colors.surfaceContainerHighest,
leading: _buildBackButton(),
flexibleSpace: FlexibleSpaceBar(
background: _buildPosterSection(movie),
),
// 右上角按钮已移到右下角悬浮按钮
);
}
/// 构建海报区域
Widget _buildPosterSection(Movie movie) {
return SizedBox.expand(
child: movie.posterPath != null && movie.posterPath!.isNotEmpty
@@ -220,79 +198,74 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
: _buildPosterPlaceholder(),
);
}
Widget _buildPosterPlaceholder() {
return const Center(
final colors = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.movie_outlined,
size: 64,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
),
SizedBox(height: 16),
const SizedBox(height: 16),
Text(
'暂无海报',
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
),
);
}
/// 构建基本信息
Widget _buildBasicInfo(Movie movie) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 影视名称
Text(
movie.title,
style: const TextStyle(
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.3,
),
),
// 别名(显示在主名称下面,用 / 分隔)
if (movie.alternateTitles.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
movie.alternateTitles.join(' / '),
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
height: 1.4,
),
),
],
const SizedBox(height: 16),
// 评分和状态
Row(
children: [
if (movie.rating != null) ...[
const Icon(
Icon(
Icons.star,
size: 20,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
const SizedBox(width: 4),
Text(
movie.rating!.toStringAsFixed(1),
style: const TextStyle(
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
const SizedBox(width: 16),
@@ -300,62 +273,56 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
_buildStatusTag(movie),
],
),
const SizedBox(height: 8),
// 上映日期
if (movie.releaseDate != null)
Text(
'${movie.releaseDate!.year}${movie.releaseDate!.month.toString().padLeft(2, '0')}月上映',
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(height: 8),
// 观看日期
if (movie.watchDate != null)
Text(
'观看于 ${_formatDate(movie.watchDate!)}',
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
),
);
}
/// 构建状态标签
Widget _buildStatusTag(Movie movie) {
final colors = Theme.of(context).colorScheme;
String label;
Color bgColor;
Color textColor;
switch (movie.status) {
case 'watched':
label = '已看';
bgColor = const Color(0xFF1A1A1A);
textColor = Colors.white;
bgColor = colors.primary;
textColor = colors.onPrimary;
break;
case 'watching':
label = '在看';
bgColor = const Color(0xFFF0F0F0);
textColor = const Color(0xFF666666);
bgColor = colors.outlineVariant;
textColor = colors.onSurface.withValues(alpha: 0.6);
break;
case 'want_to_watch':
label = '想看';
bgColor = const Color(0xFFF5F5F5);
textColor = const Color(0xFF999999);
bgColor = colors.surfaceContainerHighest;
textColor = colors.onSurface.withValues(alpha: 0.4);
break;
default:
label = '未知';
bgColor = const Color(0xFFEEEEEE);
textColor = const Color(0xFFCCCCCC);
bgColor = colors.outlineVariant;
textColor = colors.onSurface.withValues(alpha: 0.25);
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
@@ -372,30 +339,30 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
),
);
}
/// 构建导演区域
Widget _buildDirectorsSection(Movie movie) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
SizedBox(
width: 48,
child: Text(
'导演',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
),
Expanded(
child: Text(
movie.directors.join(''),
style: const TextStyle(
style: TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.5,
),
),
@@ -405,29 +372,29 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
);
}
/// 构建编剧区域
Widget _buildWritersSection(Movie movie) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
SizedBox(
width: 48,
child: Text(
'编剧',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
),
Expanded(
child: Text(
movie.writers.join(''),
style: const TextStyle(
style: TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.5,
),
),
@@ -437,29 +404,29 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
);
}
/// 构建主演区域
Widget _buildActorsSection(Movie movie) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
SizedBox(
width: 48,
child: Text(
'主演',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
),
Expanded(
child: Text(
movie.actors.join(''),
style: const TextStyle(
style: TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.5,
),
),
@@ -469,20 +436,20 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
);
}
/// 构建类型区域
Widget _buildGenresSection(Movie movie) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
SizedBox(
width: 48,
child: Text(
'类型',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
),
@@ -494,14 +461,14 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
child: Text(
genre,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
);
@@ -512,9 +479,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
),
);
}
/// 构建简介区域
Widget _buildSummarySection(Movie movie) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
@@ -526,17 +493,17 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
width: 4,
height: 16,
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.onSurface,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
const Text(
Text(
'简介',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
],
@@ -545,14 +512,14 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: Text(
movie.summary!,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.8,
),
),
@@ -561,9 +528,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
),
);
}
/// 构建额外功能区域(影评、海报墙)
Widget _buildExtraSections(Movie movie) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
@@ -575,23 +542,22 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
width: 4,
height: 16,
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.onSurface,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
const Text(
Text(
'更多',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
],
),
const SizedBox(height: 16),
// 影评入口
_buildExtraSectionItem(
icon: Icons.rate_review_outlined,
title: '影评',
@@ -601,7 +567,6 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
onTap: () => _navigateToReviews(movie),
),
const SizedBox(height: 12),
// 海报墙入口
_buildExtraSectionItem(
icon: Icons.photo_library_outlined,
title: '海报墙',
@@ -615,7 +580,6 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
);
}
/// 构建更多区域项
Widget _buildExtraSectionItem({
required IconData icon,
required String title,
@@ -624,14 +588,15 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
required String unit,
required VoidCallback onTap,
}) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Row(
children: [
@@ -639,14 +604,14 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Icon(
icon,
size: 20,
color: const Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(width: 12),
@@ -656,10 +621,10 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
children: [
Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
const SizedBox(height: 4),
@@ -669,9 +634,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
final count = snapshot.data ?? 0;
return Text(
count > 0 ? '$count $unit' : emptyText,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
);
},
@@ -679,16 +644,16 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
],
),
),
const Icon(
Icon(
Icons.chevron_right,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
),
],
),
),
);
}
void _navigateToReviews(Movie movie) {
Navigator.push(
context,
@@ -697,7 +662,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
),
);
}
void _navigateToPosters(Movie movie) {
Navigator.push(
context,
@@ -706,39 +671,38 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
),
);
}
/// 格式化日期
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
/// 跳转到编辑页面
void _navigateToEdit(BuildContext context) {
Navigator.pushNamed(context, '/movie-form', arguments: widget.movie).then((_) {
context.read<AppProvider>().loadMovies();
});
}
/// 显示删除对话框
void _showDeleteDialog(BuildContext context) {
final colors = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text(
title: Text(
'确认删除',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
content: Text(
'确定要删除"${widget.movie.title}"吗?删除后可在回收站恢复。',
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.5,
),
),
@@ -746,7 +710,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF666666),
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('取消'),
@@ -760,8 +724,8 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
ToastUtil.show(context, '已删除');
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
backgroundColor: colors.error,
foregroundColor: colors.onError,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
@@ -775,18 +739,14 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
),
);
}
/// 请求存储权限
Future<bool> _requestStoragePermission() async {
// Android 13+ 使用新的权限
if (Platform.isAndroid) {
final sdkInt = await _getAndroidSdkInt();
if (sdkInt >= 33) {
// Android 13+ 使用 READ_MEDIA_IMAGES
final status = await Permission.photos.request();
return status.isGranted;
} else {
// Android 12 及以下使用存储权限
var status = await Permission.storage.request();
if (status.isDenied) {
status = await Permission.storage.request();
@@ -794,18 +754,13 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
return status.isGranted;
}
}
// iOS 不需要额外权限来保存到应用沙盒
return true;
}
/// 获取 Android SDK 版本
Future<int> _getAndroidSdkInt() async {
// 简化处理,实际可以通过 platform channel 获取
// 这里默认返回较低版本,使用传统存储权限
return 30;
}
/// 显示分享海报页面
void _showSharePoster(Movie movie) {
Navigator.push(
context,

File diff suppressed because it is too large Load Diff

View File

@@ -45,8 +45,9 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: const Text('海报墙'),
actions: [
@@ -66,6 +67,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
}
Widget _buildEmptyState() {
final colors = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -74,21 +76,21 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
child: Icon(
Icons.photo_library_outlined,
size: 40,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
),
),
const SizedBox(height: 20),
const Text(
Text(
'暂无海报',
style: TextStyle(
fontSize: 16,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(height: 24),
@@ -97,15 +99,15 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
child: Text(
'添加记录',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
color: colors.onPrimary,
),
),
),
@@ -130,10 +132,11 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
}
Widget _buildPosterItem(MoviePoster poster, int index) {
final colors = Theme.of(context).colorScheme;
// 根据索引生成不同的高度,实现瀑布流效果
final heights = [180.0, 220.0, 160.0, 200.0, 240.0, 190.0];
final height = heights[index % heights.length];
return GestureDetector(
onTap: () => _showPosterDetail(poster),
onLongPress: () => _showDeleteDialog(poster),
@@ -158,10 +161,10 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
Image.file(
File(poster.posterPath),
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Center(
errorBuilder: (_, __, ___) => Center(
child: Icon(
Icons.broken_image,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
),
),
),
@@ -194,7 +197,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
void _showPosterDetail(MoviePoster poster) {
// 找到当前海报的索引
final initialIndex = _posters.indexWhere((p) => p.id == poster.id);
Navigator.push(
context,
MaterialPageRoute(
@@ -210,61 +213,72 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
// 显示选择对话框
final result = await showModalBottomSheet<int>(
context: context,
backgroundColor: Colors.white,
backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 顶部指示条
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: const Color(0xFFE0E0E0),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
// 标题
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
Text(
'添加海报',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
],
),
),
const SizedBox(height: 16),
// 从相册选择
_buildAddOption(
icon: Icons.photo_library_outlined,
title: '从相册选择',
subtitle: '选择本地图片',
onTap: () => Navigator.pop(context, 0),
),
// 网络链接
_buildAddOption(
icon: Icons.link_outlined,
title: '网络链接',
subtitle: '输入图片URL地址',
onTap: () => Navigator.pop(context, 1),
),
],
builder: (context) {
final colors = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(
color: colors.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 顶部指示条
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: colors.outline,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
// 标题
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
Text(
'添加海报',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
],
),
),
const SizedBox(height: 16),
// 从相册选择
_buildAddOption(
colors: colors,
icon: Icons.photo_library_outlined,
title: '从相册选择',
subtitle: '选择本地图片',
onTap: () => Navigator.pop(context, 0),
),
// 网络链接
_buildAddOption(
colors: colors,
icon: Icons.link_outlined,
title: '网络链接',
subtitle: '输入图片URL地址',
onTap: () => Navigator.pop(context, 1),
),
],
),
),
),
);
},
);
if (result == null) return;
@@ -289,10 +303,10 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
if (pickedFile != null) {
// 生成文件名
final fileName = 'posterimg_${DateTime.now().millisecondsSinceEpoch}.jpg';
// 保存到 posterimgs 子目录: images/movies/{movieId}/posterimgs/{fileName}
final targetPath = await ImagePathHelper.instance.getMoviePosterImgPath(
widget.movie.id,
widget.movie.id,
fileName
);
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
@@ -323,55 +337,58 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
/// 从网络链接添加
Future<void> _pickFromUrl() async {
final urlController = TextEditingController();
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('添加网络图片'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'请输入图片链接地址',
style: TextStyle(
fontSize: 14,
color: Color(0xFF666666),
builder: (context) {
final colors = Theme.of(context).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('添加网络图片'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'请输入图片链接地址',
style: TextStyle(
fontSize: 14,
color: colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(height: 12),
TextField(
controller: urlController,
decoration: InputDecoration(
hintText: 'https://example.com/image.jpg',
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.25)),
border: const UnderlineInputBorder(),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: colors.outline),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: colors.primary),
),
),
style: const TextStyle(fontSize: 14),
keyboardType: TextInputType.url,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
),
const SizedBox(height: 12),
TextField(
controller: urlController,
decoration: const InputDecoration(
hintText: 'https://example.com/image.jpg',
hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
border: UnderlineInputBorder(),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFFE5E5E5)),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFF1A1A1A)),
),
),
style: const TextStyle(fontSize: 14),
keyboardType: TextInputType.url,
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('确定', style: TextStyle(color: colors.onSurface)),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('确定', style: TextStyle(color: Color(0xFF1A1A1A))),
),
],
),
);
},
);
if (confirmed != true) return;
@@ -404,7 +421,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
'Referer': Uri.parse(url).replace(path: '/').toString(),
},
);
if (response.statusCode != 200) {
throw Exception('下载失败: HTTP ${response.statusCode}');
}
@@ -422,10 +439,10 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
// 生成文件名
final fileName = 'posterimg_${DateTime.now().millisecondsSinceEpoch}.jpg';
// 保存到 posterimgs 子目录
final targetPath = await ImagePathHelper.instance.getMoviePosterImgPath(
widget.movie.id,
widget.movie.id,
fileName
);
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
@@ -453,6 +470,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
/// 构建添加选项
Widget _buildAddOption({
required ColorScheme colors,
required IconData icon,
required String title,
required String subtitle,
@@ -468,13 +486,13 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
size: 22,
color: const Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(width: 16),
@@ -484,26 +502,26 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
children: [
Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
),
),
const Icon(
Icon(
Icons.chevron_right,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
size: 20,
),
],
@@ -515,28 +533,31 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
void _showDeleteDialog(MoviePoster poster) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'),
content: const Text('确定要删除这张海报吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeMoviePoster(poster.id);
Navigator.pop(context);
_loadPosters();
ToastUtil.show(context, '已删除');
},
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),
builder: (context) {
final colors = Theme.of(context).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'),
content: const Text('确定要删除这张海报吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeMoviePoster(poster.id);
Navigator.pop(context);
_loadPosters();
ToastUtil.show(context, '删除');
},
child: Text('删除', style: TextStyle(color: colors.error)),
),
],
);
},
);
}
}

View File

@@ -49,8 +49,9 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: const Text('影评详情'),
actions: [
@@ -70,9 +71,9 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
// 影评内容
Text(
_review.content,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.8,
),
),
@@ -82,7 +83,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
// 分隔线
Container(
height: 0.5,
color: const Color(0xFFE5E5E5),
color: colors.outline,
),
const SizedBox(height: 24),
@@ -92,6 +93,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
icon: Icons.person_outline,
label: '影评人:',
value: _review.reviewer.isNotEmpty ? _review.reviewer : '匿名',
colors: colors,
),
const SizedBox(height: 16),
@@ -102,6 +104,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
icon: Icons.source_outlined,
label: '来源:',
value: _review.source,
colors: colors,
),
if (_review.source.isNotEmpty) const SizedBox(height: 16),
@@ -111,6 +114,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
icon: Icons.category_outlined,
label: '类型:',
value: _review.typeText,
colors: colors,
),
const SizedBox(height: 16),
@@ -120,6 +124,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
icon: Icons.access_time,
label: '时间:',
value: _formatDate(_review.createdAt),
colors: colors,
),
],
),
@@ -132,13 +137,14 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
required IconData icon,
required String label,
required String value,
required ColorScheme colors,
}) {
return Row(
children: [
Icon(
icon,
size: 20,
color: const Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
const SizedBox(width: 12),
// 固定宽度容器,以"影评人:"的最大宽度为准
@@ -146,18 +152,18 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
width: 64,
child: Text(
label,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
),

View File

@@ -46,10 +46,11 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final isEdit = widget.review != null;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: Text(isEdit ? '编辑影评' : '写影评'),
actions: [
@@ -73,24 +74,24 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
// 顶部信息栏
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: const BoxDecoration(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
bottom: BorderSide(color: colors.outline, width: 0.5),
),
),
child: Row(
children: [
// 类型选择
_buildTypeSelector(),
_buildTypeSelector(colors),
const SizedBox(width: 16),
// 评论人
Expanded(
child: TextField(
controller: _reviewerController,
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
decoration: const InputDecoration(
style: TextStyle(fontSize: 14, color: colors.onSurface),
decoration: InputDecoration(
hintText: '评论人',
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
@@ -103,10 +104,10 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
width: 100,
child: TextField(
controller: _sourceController,
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
decoration: const InputDecoration(
style: TextStyle(fontSize: 14, color: colors.onSurface),
decoration: InputDecoration(
hintText: '来源',
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
@@ -116,7 +117,7 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
],
),
),
// 评论内容区域
Expanded(
child: TextFormField(
@@ -124,19 +125,19 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.7,
),
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: '写下你的影评...',
hintStyle: TextStyle(
fontSize: 16,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
),
border: InputBorder.none,
contentPadding: EdgeInsets.all(16),
contentPadding: const EdgeInsets.all(16),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
@@ -153,29 +154,29 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
}
/// 构建类型选择器
Widget _buildTypeSelector() {
Widget _buildTypeSelector(ColorScheme colors) {
return GestureDetector(
onTap: () => _showTypeSelector(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)),
border: Border.all(color: colors.outline),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_reviewType == 1 ? '短评' : '长评',
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(width: 4),
const Icon(
Icon(
Icons.arrow_drop_down,
size: 16,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
],
),
@@ -187,36 +188,42 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
void _showTypeSelector() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('短评'),
trailing: _reviewType == 1
? const Icon(Icons.check, color: Color(0xFF1A1A1A))
: null,
onTap: () {
setState(() => _reviewType = 1);
Navigator.pop(context);
},
builder: (context) {
final colors = Theme.of(context).colorScheme;
return Container(
color: colors.surface,
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('短评'),
trailing: _reviewType == 1
? Icon(Icons.check, color: colors.onSurface)
: null,
onTap: () {
setState(() => _reviewType = 1);
Navigator.pop(context);
},
),
Divider(height: 0.5, color: colors.outline),
ListTile(
title: const Text('长评'),
trailing: _reviewType == 2
? Icon(Icons.check, color: colors.onSurface)
: null,
onTap: () {
setState(() => _reviewType = 2);
Navigator.pop(context);
},
),
],
),
const Divider(height: 0.5),
ListTile(
title: const Text('长评'),
trailing: _reviewType == 2
? const Icon(Icons.check, color: Color(0xFF1A1A1A))
: null,
onTap: () {
setState(() => _reviewType = 2);
Navigator.pop(context);
},
),
],
),
),
),
);
},
);
}

View File

@@ -73,19 +73,20 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: _isSearching
? TextField(
controller: _searchController,
autofocus: true,
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: '搜索影评内容、评论人、来源...',
hintStyle: TextStyle(color: Color(0xFF999999)),
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)),
border: InputBorder.none,
),
style: const TextStyle(color: Color(0xFF1A1A1A)),
style: TextStyle(color: colors.onSurface),
onChanged: _onSearchChanged,
)
: const Text('影评'),
@@ -112,6 +113,7 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
}
Widget _buildEmptyState() {
final colors = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -120,21 +122,21 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
child: Icon(
Icons.rate_review_outlined,
size: 40,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
),
),
const SizedBox(height: 20),
const Text(
Text(
'暂无影评',
style: TextStyle(
fontSize: 16,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(height: 24),
@@ -143,15 +145,15 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
child: Text(
'添加记录',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
color: colors.onPrimary,
),
),
),
@@ -176,13 +178,14 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
}
Widget _buildReviewCard(MovieReview review) {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: () => _navigateToReviewDetail(review),
onLongPress: () => _showDeleteDialog(review),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Column(
@@ -193,8 +196,8 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: review.reviewType == 1
? Colors.white
: const Color(0xFF1A1A1A),
? colors.surface
: colors.primary,
borderRadius: BorderRadius.circular(4),
),
child: Text(
@@ -202,8 +205,8 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
style: TextStyle(
fontSize: 10,
color: review.reviewType == 1
? const Color(0xFF666666)
: Colors.white,
? colors.onSurface.withValues(alpha: 0.6)
: colors.onPrimary,
),
),
),
@@ -215,9 +218,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
review.content,
maxLines: review.reviewType == 1 ? 4 : 8,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.5,
),
),
@@ -232,9 +235,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
Expanded(
child: Text(
review.reviewer,
style: const TextStyle(
style: TextStyle(
fontSize: 11,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
overflow: TextOverflow.ellipsis,
),
@@ -252,9 +255,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
Expanded(
child: Text(
review.source,
style: const TextStyle(
style: TextStyle(
fontSize: 10,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
overflow: TextOverflow.ellipsis,
),
@@ -262,9 +265,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
// 日期
Text(
_formatDate(review.createdAt),
style: const TextStyle(
style: TextStyle(
fontSize: 10,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
@@ -315,28 +318,31 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
void _showDeleteDialog(MovieReview review) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'),
content: const Text('确定要删除这条影评吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeMovieReview(review.id);
Navigator.pop(context);
_loadReviews();
ToastUtil.show(context, '已删除');
},
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),
builder: (context) {
final colors = Theme.of(context).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'),
content: const Text('确定要删除这条影评吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeMovieReview(review.id);
Navigator.pop(context);
_loadReviews();
ToastUtil.show(context, '删除');
},
child: Text('删除', style: TextStyle(color: colors.error)),
),
],
);
},
);
}
}
}

View File

@@ -24,21 +24,22 @@ class _MovieSharePageState extends State<MovieSharePage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
backgroundColor: colors.surfaceContainerHighest,
appBar: AppBar(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)),
icon: Icon(Icons.close, color: colors.onSurface),
onPressed: () => Navigator.pop(context),
),
title: const Text(
title: Text(
'分享海报',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
centerTitle: true,
@@ -51,12 +52,12 @@ class _MovieSharePageState extends State<MovieSharePage> {
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text(
: Text(
'分享',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
),
@@ -77,13 +78,14 @@ class _MovieSharePageState extends State<MovieSharePage> {
/// 构建海报 Widget
Widget _buildPosterWidget() {
final colors = Theme.of(context).colorScheme;
final movie = widget.movie;
final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty;
return Container(
width: 320,
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
@@ -118,10 +120,10 @@ class _MovieSharePageState extends State<MovieSharePage> {
// 标题
Text(
movie.title,
style: const TextStyle(
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
@@ -130,9 +132,9 @@ class _MovieSharePageState extends State<MovieSharePage> {
const SizedBox(height: 8),
Text(
movie.alternateTitles.join(' / '),
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
],
@@ -158,11 +160,11 @@ class _MovieSharePageState extends State<MovieSharePage> {
),
),
const SizedBox(width: 4),
const Text(
Text(
'/ 10',
style: TextStyle(
fontSize: 12,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
@@ -172,44 +174,45 @@ class _MovieSharePageState extends State<MovieSharePage> {
// 导演
if (movie.directors.isNotEmpty)
_buildInfoRow('导演', movie.directors.join(' / ')),
_buildInfoRow('导演', movie.directors.join(' / '), colors),
// 编剧
if (movie.writers.isNotEmpty)
_buildInfoRow('编剧', movie.writers.join(' / ')),
_buildInfoRow('编剧', movie.writers.join(' / '), colors),
// 主演
if (movie.actors.isNotEmpty)
_buildInfoRow('主演', movie.actors.take(3).join(' / ')),
_buildInfoRow('主演', movie.actors.take(3).join(' / '), colors),
// 类型
if (movie.genres.isNotEmpty)
_buildInfoRow('类型', movie.genres.join(' / ')),
_buildInfoRow('类型', movie.genres.join(' / '), colors),
// 上映日期
if (movie.releaseDate != null)
_buildInfoRow(
'上映',
'${movie.releaseDate!.year}.${movie.releaseDate!.month.toString().padLeft(2, '0')}.${movie.releaseDate!.day.toString().padLeft(2, '0')}',
colors,
),
const SizedBox(height: 16),
// 简介
if (movie.summary != null && movie.summary!.isNotEmpty) ...[
const Text(
Text(
'简介',
style: TextStyle(
fontSize: 12,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(height: 8),
Text(
movie.summary!,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.6,
),
maxLines: 5,
@@ -220,7 +223,7 @@ class _MovieSharePageState extends State<MovieSharePage> {
const SizedBox(height: 20),
// 底部标识
const Divider(height: 1, color: Color(0xFFE8E8E8)),
Divider(height: 1, color: colors.outline),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -228,14 +231,14 @@ class _MovieSharePageState extends State<MovieSharePage> {
Icon(
Icons.movie_outlined,
size: 14,
color: const Color(0xFF1A1A1A).withOpacity(0.5),
color: colors.onSurface.withValues(alpha: 0.5),
),
const SizedBox(width: 6),
Text(
'来自 MookNote',
style: TextStyle(
fontSize: 12,
color: const Color(0xFF1A1A1A).withOpacity(0.5),
color: colors.onSurface.withValues(alpha: 0.5),
),
),
],
@@ -249,7 +252,7 @@ class _MovieSharePageState extends State<MovieSharePage> {
}
/// 构建信息行
Widget _buildInfoRow(String label, String value) {
Widget _buildInfoRow(String label, String value, ColorScheme colors) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
@@ -257,17 +260,17 @@ class _MovieSharePageState extends State<MovieSharePage> {
children: [
Text(
'$label',
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF333333),
color: colors.onSurface.withValues(alpha: 0.75),
),
),
),

View File

@@ -17,7 +17,7 @@ class MovieTabPage extends StatefulWidget {
}
class _MovieTabPageState extends State<MovieTabPage> {
int _layoutStyle = 0; // 0: 海报网格, 1: 列表
int _layoutStyle = 0;
bool _firstLoad = true;
@override
@@ -31,10 +31,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Column(
children: [
const MovieStatusBar(),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
Expanded(
child: _buildMovieList(context),
),
@@ -43,12 +44,12 @@ class _MovieTabPageState extends State<MovieTabPage> {
}
Widget _buildMovieList(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Consumer<AppProvider>(
builder: (context, provider, child) {
final statusMap = {0: 'watched', 1: 'watching', 2: 'want_to_watch'};
final currentStatus = statusMap[provider.movieStatusIndex]!;
final allMovies = provider.movies.where((m) => !m.isDeleted).toList();
// 首次加载且数据为空时才显示骨架屏
if (_firstLoad && allMovies.isEmpty) {
return _buildSkeleton();
}
@@ -59,8 +60,8 @@ class _MovieTabPageState extends State<MovieTabPage> {
if (movies.isEmpty) {
return RefreshIndicator(
onRefresh: () async => await provider.loadMovies(),
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
color: colors.primary,
backgroundColor: colors.surface,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [_buildEmptyState(context, provider.movieStatusIndex)],
@@ -77,10 +78,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
}
Widget _buildGridView(List movies, AppProvider provider) {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator(
onRefresh: () async => await provider.loadMovies(),
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
color: colors.primary,
backgroundColor: colors.surface,
child: GridView.builder(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 100),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
@@ -96,10 +98,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
}
Widget _buildListView(List movies, AppProvider provider) {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator(
onRefresh: () async => await provider.loadMovies(),
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
color: colors.primary,
backgroundColor: colors.surface,
child: ListView.builder(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
itemCount: movies.length,
@@ -109,6 +112,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
}
Widget _buildListCard(movie) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: movie),
onLongPress: () => _showDeleteDialog(context, movie),
@@ -116,23 +120,22 @@ class _MovieTabPageState extends State<MovieTabPage> {
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
// 海报缩略图
Container(
width: 48, height: 64,
decoration: BoxDecoration(
color: const Color(0xFFF0F0F0),
color: colors.outlineVariant,
borderRadius: BorderRadius.circular(6),
),
clipBehavior: Clip.antiAlias,
child: movie.posterPath != null && movie.posterPath!.isNotEmpty
? Image.file(File(movie.posterPath!), fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Icon(Icons.movie_outlined, size: 22, color: Color(0xFFCCCCCC)))
: const Icon(Icons.movie_outlined, size: 22, color: Color(0xFFCCCCCC)),
errorBuilder: (_, __, ___) => Icon(Icons.movie_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)))
: Icon(Icons.movie_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)),
),
const SizedBox(width: 12),
Expanded(
@@ -140,11 +143,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(movie.title, maxLines: 1, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
if (movie.alternateTitles.isNotEmpty) ...[
const SizedBox(height: 3),
Text(movie.alternateTitles.take(2).join(''), maxLines: 1, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
],
const SizedBox(height: 6),
if (movie.rating != null)
@@ -155,7 +158,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
),
),
const SizedBox(width: 8),
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
],
),
),
@@ -163,19 +166,20 @@ class _MovieTabPageState extends State<MovieTabPage> {
}
void _showDeleteDialog(BuildContext context, movie) {
final colors = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
content: Text('确定要删除《${movie.title}》吗?删除后可在回收站恢复。',
style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)),
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
),
ElevatedButton(
onPressed: () async {
@@ -183,7 +187,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
Navigator.pop(ctx);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, foregroundColor: Colors.white, elevation: 0,
backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
@@ -200,6 +204,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
}
Widget _buildListSkeleton() {
final colors = Theme.of(context).colorScheme;
return ListView.builder(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
itemCount: 6,
@@ -207,7 +212,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: const Row(
@@ -235,6 +240,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
}
Widget _buildEmptyState(BuildContext context, int statusIndex) {
final colors = Theme.of(context).colorScheme;
final statusText = ['已看', '在看', '想看'][statusIndex];
return Center(
child: Column(
@@ -242,11 +248,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
children: [
Container(
width: 80, height: 80,
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(20)),
child: const Icon(Icons.movie_outlined, size: 40, color: Color(0xFFCCCCCC)),
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)),
child: Icon(Icons.movie_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
),
const SizedBox(height: 20),
Text('暂无$statusText的影片', style: const TextStyle(fontSize: 16, color: Color(0xFF999999))),
Text('暂无$statusText的影片', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 24),
InkWell(
onTap: () {
@@ -255,8 +261,8 @@ class _MovieTabPageState extends State<MovieTabPage> {
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(8)),
child: const Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(8)),
child: Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)),
),
),
],

View File

@@ -22,13 +22,14 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final note = context.watch<AppProvider>().notes.firstWhere(
(n) => n.id == widget.note.id,
orElse: () => widget.note,
);
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: Text(
note.title.isNotEmpty
@@ -42,22 +43,21 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
children: [
Column(
children: [
// 日期 + 字数
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: const BoxDecoration(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
bottom: BorderSide(color: colors.outlineVariant, width: 0.5),
),
),
child: Row(
children: [
Text(
'${note.createdAt.day}',
style: const TextStyle(
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.w200,
color: Color(0xFF333333),
color: colors.onSurface.withValues(alpha: 0.75),
height: 1.0,
),
),
@@ -68,48 +68,44 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
children: [
Text(
'${note.createdAt.year}/${note.createdAt.month.toString().padLeft(2, '0')}${_weekdays[note.createdAt.weekday - 1]}',
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: Color(0xFF777777)),
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.55)),
),
const SizedBox(height: 1),
Text(
'${note.createdAt.hour.toString().padLeft(2, '0')}:${note.createdAt.minute.toString().padLeft(2, '0')}',
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)),
),
],
),
const Spacer(),
Text(
'${note.content.length}',
style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)),
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35)),
),
],
),
),
// 标签行
if (note.tags.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 6),
child: _buildTagRow(note.tags),
),
// 内容
Expanded(
child: Markdown(
data: note.content,
styleSheet: _buildMarkdownStyleSheet(),
styleSheet: _buildMarkdownStyleSheet(colors),
padding: const EdgeInsets.all(16),
// ignore: deprecated_member_use
imageBuilder: (uri, title, alt) => _buildMarkdownImage(uri, note),
),
),
// 图片行
if (note.images.isNotEmpty) _buildImageRow(note.images),
],
),
// 右下角悬浮按钮组
Positioned(
right: 16,
bottom: 24,
@@ -121,6 +117,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
}
Widget _buildTagRow(List<String> tags) {
final colors = Theme.of(context).colorScheme;
return Container(
height: 28,
margin: const EdgeInsets.only(bottom: 2),
@@ -133,12 +130,12 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
),
child: Text(
tags[index],
style: const TextStyle(fontSize: 11, color: Color(0xFF666666)),
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6)),
),
);
},
@@ -147,12 +144,13 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
}
Widget _buildImageRow(List<String> images) {
final colors = Theme.of(context).colorScheme;
return Container(
height: 80,
decoration: BoxDecoration(
color: Colors.white,
border: const Border(
top: BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
color: colors.surface,
border: Border(
top: BorderSide(color: colors.outlineVariant, width: 0.5),
),
boxShadow: [
BoxShadow(
@@ -175,15 +173,15 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
height: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
clipBehavior: Clip.antiAlias,
child: Image.file(
File(images[index]),
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
color: const Color(0xFFF5F5F5),
child: const Icon(Icons.broken_image_outlined, size: 20, color: Color(0xFFCCCCCC)),
color: colors.surfaceContainerHighest,
child: Icon(Icons.broken_image_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.25)),
),
),
),
@@ -193,37 +191,37 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
);
}
MarkdownStyleSheet _buildMarkdownStyleSheet() {
MarkdownStyleSheet _buildMarkdownStyleSheet(ColorScheme colors) {
return MarkdownStyleSheet(
h1: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4),
h2: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4),
h3: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4),
h4: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4),
p: const TextStyle(fontSize: 15, color: Color(0xFF333333), height: 1.8),
code: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A), backgroundColor: Color(0xFFF5F5F5), fontFamily: 'monospace'),
h1: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4),
h2: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4),
h3: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4),
h4: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4),
p: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.75), height: 1.8),
code: TextStyle(fontSize: 14, color: colors.onSurface, backgroundColor: colors.surfaceContainerHighest, fontFamily: 'monospace'),
codeblockDecoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
border: Border.all(color: const Color(0xFFE5E5E5)),
color: colors.surfaceContainerHigh,
border: Border.all(color: colors.outline),
borderRadius: BorderRadius.circular(6),
),
codeblockPadding: const EdgeInsets.all(12),
blockquote: const TextStyle(fontSize: 15, color: Color(0xFF666666), fontStyle: FontStyle.italic, height: 1.8),
blockquoteDecoration: const BoxDecoration(
border: Border(left: BorderSide(color: Color(0xFF999999), width: 4)),
blockquote: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.6), fontStyle: FontStyle.italic, height: 1.8),
blockquoteDecoration: BoxDecoration(
border: Border(left: BorderSide(color: colors.onSurface.withValues(alpha: 0.4), width: 4)),
),
blockquotePadding: const EdgeInsets.only(left: 12, top: 4, bottom: 4),
listBullet: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
listBullet: TextStyle(fontSize: 15, color: colors.onSurface),
listIndent: 24,
a: const TextStyle(fontSize: 15, color: Color(0xFF4A90D9), decoration: TextDecoration.underline),
tableHead: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
tableBody: const TextStyle(fontSize: 14, color: Color(0xFF333333)),
tableBorder: TableBorder.all(color: const Color(0xFFE5E5E5), width: 0.5),
tableHead: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface),
tableBody: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75)),
tableBorder: TableBorder.all(color: colors.outline, width: 0.5),
tableColumnWidth: const FlexColumnWidth(),
tableCellsDecoration: const BoxDecoration(color: Colors.white),
tableCellsDecoration: BoxDecoration(color: colors.surface),
tablePadding: const EdgeInsets.all(8),
strong: const TextStyle(fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
em: const TextStyle(fontStyle: FontStyle.italic, color: Color(0xFF333333)),
del: const TextStyle(decoration: TextDecoration.lineThrough, color: Color(0xFF999999)),
strong: TextStyle(fontWeight: FontWeight.w600, color: colors.onSurface),
em: TextStyle(fontStyle: FontStyle.italic, color: colors.onSurface.withValues(alpha: 0.75)),
del: TextStyle(decoration: TextDecoration.lineThrough, color: colors.onSurface.withValues(alpha: 0.4)),
);
}
@@ -272,7 +270,6 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
);
}
/// 截取内容前N个字作为标题
String _truncateContent(String content) {
final cleaned = content
.replaceAll(RegExp(r'^#+\s+', multiLine: true), '')
@@ -298,8 +295,8 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
});
}
/// 右下角悬浮按钮组
Widget _buildFloatingActionButtons() {
final colors = Theme.of(context).colorScheme;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -307,13 +304,16 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
icon: Icons.edit_outlined,
onPressed: () => _navigateToEdit(context),
tooltip: '编辑',
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
),
const SizedBox(height: 12),
_buildFloatingButton(
icon: Icons.delete_outline,
onPressed: () => _showDeleteDialog(context),
tooltip: '删除',
backgroundColor: Colors.red,
backgroundColor: colors.error,
foregroundColor: colors.onError,
),
const SizedBox(height: 12),
_buildFloatingButton(
@@ -321,6 +321,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
onPressed: _shareNote,
tooltip: '分享',
backgroundColor: const Color(0xFF4CAF50),
foregroundColor: Colors.white,
),
],
);
@@ -330,7 +331,8 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
required IconData icon,
required VoidCallback onPressed,
required String tooltip,
Color backgroundColor = const Color(0xFF1A1A1A),
required Color backgroundColor,
required Color foregroundColor,
}) {
return Material(
color: Colors.transparent,
@@ -349,7 +351,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
],
),
child: IconButton(
icon: Icon(icon, size: 18, color: Colors.white),
icon: Icon(icon, size: 18, color: foregroundColor),
onPressed: onPressed,
padding: EdgeInsets.zero,
tooltip: tooltip,
@@ -375,7 +377,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
context.read<AppProvider>().removeNote(widget.note.id);
Navigator.pop(context);
},
child: const Text('删除', style: TextStyle(color: Colors.red)),
child: Text('删除', style: TextStyle(color: Theme.of(context).colorScheme.error)),
),
],
),

View File

@@ -59,8 +59,9 @@ class _NoteFormPageState extends State<NoteFormPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: GestureDetector(
@@ -73,8 +74,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
child: TextButton(
onPressed: _saveNote,
style: TextButton.styleFrom(
backgroundColor: const Color(0xFF1A1A1A),
foregroundColor: Colors.white,
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
@@ -96,9 +97,9 @@ class _NoteFormPageState extends State<NoteFormPage> {
// 顶部信息栏
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: const BoxDecoration(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
bottom: BorderSide(color: colors.outline, width: 0.5),
),
),
child: Column(
@@ -110,10 +111,10 @@ class _NoteFormPageState extends State<NoteFormPage> {
// 大日号
Text(
'${_createdAt.day}',
style: const TextStyle(
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.w200,
color: Color(0xFF333333),
color: colors.onSurface.withValues(alpha: 0.75),
height: 1.0,
),
),
@@ -125,18 +126,18 @@ class _NoteFormPageState extends State<NoteFormPage> {
children: [
Text(
'${_createdAt.year}/${_createdAt.month.toString().padLeft(2, '0')}${_weekdays[_createdAt.weekday - 1]}',
style: const TextStyle(
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: Color(0xFF777777),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(height: 1),
Text(
'${_createdAt.hour.toString().padLeft(2, '0')}:${_createdAt.minute.toString().padLeft(2, '0')}',
style: const TextStyle(
style: TextStyle(
fontSize: 11,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
@@ -145,9 +146,9 @@ class _NoteFormPageState extends State<NoteFormPage> {
// 字数统计
Text(
'$_charCount',
style: const TextStyle(
style: TextStyle(
fontSize: 11,
color: Color(0xFFAAAAAA),
color: colors.onSurface.withValues(alpha: 0.35),
),
),
],
@@ -208,13 +209,14 @@ class _NoteFormPageState extends State<NoteFormPage> {
/// 现代极简底部工具栏
Widget _buildFloatingToolbar() {
final colors = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.fromLTRB(12, 8, 12, 10),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFEAEAEA), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
@@ -251,7 +253,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
// 视图模式切换
const SizedBox(width: 8),
Container(
width: 1, height: 20, color: const Color(0xFFE5E5E5),
width: 1, height: 20, color: colors.outline,
),
const SizedBox(width: 8),
_modeSwitch(Icons.edit, 'edit'),
@@ -262,6 +264,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
}
Widget _toolBtn(IconData icon, String tooltip, VoidCallback onTap) {
final colors = Theme.of(context).colorScheme;
return Material(
color: Colors.transparent,
child: InkWell(
@@ -271,7 +274,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
message: tooltip,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 6),
child: Icon(icon, size: 21, color: const Color(0xFF555555)),
child: Icon(icon, size: 21, color: colors.onSurface.withValues(alpha: 0.7)),
),
),
),
@@ -279,16 +282,18 @@ class _NoteFormPageState extends State<NoteFormPage> {
}
Widget _toolGap() {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 6),
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: SizedBox(
height: 18,
child: VerticalDivider(width: 0, thickness: 0.5, color: Color(0xFFE0E0E0)),
child: VerticalDivider(width: 0, thickness: 0.5, color: colors.outline),
),
);
}
Widget _modeSwitch(IconData icon, String mode) {
final colors = Theme.of(context).colorScheme;
final active = _editorMode == mode;
return Material(
color: Colors.transparent,
@@ -298,13 +303,13 @@ class _NoteFormPageState extends State<NoteFormPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 6),
decoration: BoxDecoration(
color: active ? const Color(0xFF1A1A1A) : Colors.transparent,
color: active ? colors.primary : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
size: 18,
color: active ? Colors.white : const Color(0xFFAAAAAA),
color: active ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.35),
),
),
),
@@ -374,6 +379,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
/// 编辑器
Widget _buildEditor() {
final colors = Theme.of(context).colorScheme;
return TextField(
controller: _contentController,
maxLines: null,
@@ -384,40 +390,41 @@ class _NoteFormPageState extends State<NoteFormPage> {
height: 1.6,
fontSize: 16,
),
style: const TextStyle(
style: TextStyle(
fontSize: 16,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.6,
),
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: '使用 Markdown 格式书写...',
hintStyle: TextStyle(
fontSize: 16,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
height: 1.6,
),
border: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.all(16),
contentPadding: const EdgeInsets.all(16),
),
);
}
/// 实时 Markdown 预览(点击回到编辑)
Widget _buildPreview() {
final colors = Theme.of(context).colorScheme;
final text = _contentController.text;
return GestureDetector(
onTap: () => setState(() => _editorMode = 'edit'),
behavior: HitTestBehavior.opaque,
child: Container(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
child: text.isEmpty
? const Center(
? Center(
child: Text(
'预览区域',
style: TextStyle(
fontSize: 14,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
),
),
)
@@ -426,45 +433,45 @@ class _NoteFormPageState extends State<NoteFormPage> {
selectable: true,
padding: const EdgeInsets.all(16),
styleSheet: MarkdownStyleSheet(
h1: const TextStyle(
h1: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.4,
),
h2: const TextStyle(
h2: TextStyle(
fontSize: 19,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.4,
),
h3: const TextStyle(
h3: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.4,
),
p: const TextStyle(
p: TextStyle(
fontSize: 15,
color: Color(0xFF333333),
color: colors.onSurface.withValues(alpha: 0.75),
height: 1.7,
),
code: const TextStyle(
code: TextStyle(
fontSize: 14,
color: Color(0xFF1A1A1A),
backgroundColor: Color(0xFFF0F0F0),
color: colors.onSurface,
backgroundColor: colors.outlineVariant,
),
codeblockDecoration: BoxDecoration(
color: const Color(0xFFF0F0F0),
color: colors.outlineVariant,
borderRadius: BorderRadius.circular(6),
),
blockquote: const TextStyle(
blockquote: TextStyle(
fontSize: 15,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
blockquoteDecoration: const BoxDecoration(
blockquoteDecoration: BoxDecoration(
border: Border(
left: BorderSide(color: Color(0xFFCCCCCC), width: 3),
left: BorderSide(color: colors.onSurface.withValues(alpha: 0.25), width: 3),
),
),
),
@@ -494,10 +501,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
}
Widget _buildTagChip(int index) {
final colors = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
),
child: Row(
@@ -505,12 +513,12 @@ class _NoteFormPageState extends State<NoteFormPage> {
children: [
Text(
_tags[index],
style: const TextStyle(fontSize: 11, color: Color(0xFF666666)),
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6)),
),
const SizedBox(width: 3),
GestureDetector(
onTap: () => setState(() => _tags.removeAt(index)),
child: const Icon(Icons.close, size: 10, color: Color(0xFFBBBBBB)),
child: Icon(Icons.close, size: 10, color: colors.onSurface.withValues(alpha: 0.3)),
),
],
),
@@ -518,20 +526,21 @@ class _NoteFormPageState extends State<NoteFormPage> {
}
Widget _buildAddTagButton() {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: _showAddTagDialog,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFDDDDDD), width: 1),
border: Border.all(color: colors.onSurface.withValues(alpha: 0.25), width: 1),
),
child: const Row(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add, size: 12, color: Color(0xFFAAAAAA)),
SizedBox(width: 2),
Text('标签', style: TextStyle(fontSize: 11, color: Color(0xFFAAAAAA))),
Icon(Icons.add, size: 12, color: colors.onSurface.withValues(alpha: 0.35)),
const SizedBox(width: 2),
Text('标签', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))),
],
),
),
@@ -550,18 +559,20 @@ class _NoteFormPageState extends State<NoteFormPage> {
showDialog(
context: context,
builder: (ctx) => StatefulBuilder(
builder: (ctx) {
final colors = Theme.of(context).colorScheme;
return StatefulBuilder(
builder: (ctx, setDialogState) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
title: const Text(
title: Text(
'添加标签',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
@@ -575,13 +586,13 @@ class _NoteFormPageState extends State<NoteFormPage> {
TextField(
controller: controller,
autofocus: true,
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
cursorColor: const Color(0xFF1A1A1A),
style: TextStyle(fontSize: 14, color: colors.onSurface),
cursorColor: colors.primary,
decoration: InputDecoration(
hintText: '输入新标签名称',
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFBBBBBB)),
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.3)),
filled: true,
fillColor: const Color(0xFFF8F8F8),
fillColor: colors.surfaceContainerHigh,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
@@ -593,11 +604,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1),
borderSide: BorderSide(color: colors.primary, width: 1),
),
suffixIcon: controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, size: 16, color: Color(0xFFAAAAAA)),
icon: Icon(Icons.clear, size: 16, color: colors.onSurface.withValues(alpha: 0.35)),
onPressed: () => controller.clear(),
)
: null,
@@ -613,11 +624,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
// 已有标签列表
if (availableTags.isNotEmpty) ...[
const SizedBox(height: 20),
const Text(
Text(
'或选择已有标签',
style: TextStyle(
fontSize: 12,
color: Color(0xFFAAAAAA),
color: colors.onSurface.withValues(alpha: 0.35),
),
),
const SizedBox(height: 12),
@@ -637,15 +648,15 @@ class _NoteFormPageState extends State<NoteFormPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outline, width: 0.5),
),
child: Text(
tag,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF555555),
color: colors.onSurface.withValues(alpha: 0.7),
),
),
),
@@ -673,7 +684,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
TextButton(
onPressed: () => Navigator.pop(ctx),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF999999),
foregroundColor: colors.onSurface.withValues(alpha: 0.4),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('取消', style: TextStyle(fontSize: 14)),
@@ -684,8 +695,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
Navigator.pop(ctx);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1A1A1A),
foregroundColor: Colors.white,
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
@@ -695,10 +706,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
],
actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
),
),
);
},
);
}
/// 获取所有已有标签(从所有笔记中收集)
List<String> _getAllExistingTags(AppProvider provider) {
final allTags = <String>{};
@@ -717,6 +729,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
}
Widget _buildAppBarTitle() {
final colors = Theme.of(context).colorScheme;
final base = _isEditing ? '编辑笔记' : '新建笔记';
final t = _titleController.text.trim();
if (t.isEmpty) {
@@ -725,7 +738,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
children: [
Text(base),
const SizedBox(width: 6),
const Icon(Icons.edit, size: 14, color: Color(0xFF999999)),
Icon(Icons.edit, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
],
);
}
@@ -738,8 +751,10 @@ class _NoteFormPageState extends State<NoteFormPage> {
final controller = TextEditingController(text: _titleController.text);
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
builder: (context) {
final colors = Theme.of(context).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text(
@@ -749,23 +764,23 @@ class _NoteFormPageState extends State<NoteFormPage> {
content: TextField(
controller: controller,
autofocus: true,
style: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A)),
style: TextStyle(fontSize: 16, color: colors.onSurface),
decoration: InputDecoration(
hintText: '输入标题...',
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
filled: true,
fillColor: const Color(0xFFFAFAFA),
fillColor: colors.surfaceContainerHigh,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
borderSide: BorderSide(color: colors.outline, width: 0.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
borderSide: BorderSide(color: colors.outline, width: 0.5),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1),
borderSide: BorderSide(color: colors.primary, width: 1),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
@@ -778,7 +793,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF666666),
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
),
child: const Text('取消'),
),
@@ -788,8 +803,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1A1A1A),
foregroundColor: Colors.white,
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
@@ -797,7 +812,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
),
],
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
);
},
);
}
@@ -826,7 +842,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
} else {
// 添加新笔记 - 先创建笔记获取ID
final noteId = now.millisecondsSinceEpoch.toString();
// 如果有图片需要移动到正确的ID目录
List<String> finalImages = [];
if (_images.isNotEmpty) {
@@ -835,7 +851,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
final newNoteId = noteId;
finalImages = await _moveImagesToNewId(oldNoteId, newNoteId);
}
final title = _titleController.text.trim();
final newNote = Note(
@@ -856,17 +872,17 @@ class _NoteFormPageState extends State<NoteFormPage> {
// 刷新笔记列表
await context.read<AppProvider>().loadNotes();
if (!mounted) return;
Navigator.pop(context);
}
/// 将图片从临时ID目录移动到新ID目录
Future<List<String>> _moveImagesToNewId(String oldNoteId, String newNoteId) async {
final List<String> newPaths = [];
final newDir = await ImagePathHelper.instance.getNoteImagesDir(newNoteId);
for (final imagePath in _images) {
// 使用路径分隔符检查,兼容 Windows 和 Unix
final normalizedPath = imagePath.replaceAll('\\', '/');
@@ -874,9 +890,9 @@ class _NoteFormPageState extends State<NoteFormPage> {
// 需要移动的文件
final fileName = p.basename(imagePath);
final newPath = p.join(newDir, fileName);
await ImagePathHelper.instance.ensureDirExists(newDir);
// 检查源文件是否存在
final sourceFile = File(imagePath);
if (await sourceFile.exists()) {
@@ -888,14 +904,14 @@ class _NoteFormPageState extends State<NoteFormPage> {
newPaths.add(imagePath);
}
}
// 删除旧目录
try {
await ImagePathHelper.instance.deleteNoteImages(oldNoteId);
} catch (e) {
// 忽略删除失败
}
return newPaths;
}
@@ -908,11 +924,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
maxHeight: 1920,
imageQuality: 85,
);
if (image != null) {
// 生成唯一的文件名
final fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg';
// 如果是编辑模式使用现有笔记ID如果是新建模式使用临时ID保存时会替换
String noteId;
if (_isEditing) {
@@ -922,14 +938,14 @@ class _NoteFormPageState extends State<NoteFormPage> {
noteId = _tempNoteId ?? DateTime.now().millisecondsSinceEpoch.toString();
_tempNoteId = noteId;
}
// 复制图片到应用目录: images/notes/{noteId}/{fileName}
final targetDir = await ImagePathHelper.instance.getNoteImagesDir(noteId);
await ImagePathHelper.instance.ensureDirExists(targetDir);
final targetPath = p.join(targetDir, fileName);
await File(image.path).copy(targetPath);
setState(() => _images.add(targetPath));
}
} catch (e) {
@@ -939,11 +955,12 @@ class _NoteFormPageState extends State<NoteFormPage> {
/// 构建图片横向滚动行
Widget _buildImageRow() {
final colors = Theme.of(context).colorScheme;
if (_images.isEmpty) {
return Container(
height: 80,
decoration: const BoxDecoration(
color: Colors.white,
decoration: BoxDecoration(
color: colors.surface,
),
child: ListView(
scrollDirection: Axis.horizontal,
@@ -958,7 +975,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
return Container(
height: 88,
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.02),
@@ -984,6 +1001,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
/// 添加图片按钮
Widget _buildAddImageButton() {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: _pickImage,
borderRadius: BorderRadius.circular(12),
@@ -992,13 +1010,13 @@ class _NoteFormPageState extends State<NoteFormPage> {
height: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: const Color(0xFFF8F8F8),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
color: colors.surfaceContainerHigh,
border: Border.all(color: colors.outline, width: 0.5),
),
child: const Icon(
child: Icon(
Icons.add_photo_alternate_outlined,
size: 24,
color: Color(0xFFBBBBBB),
color: colors.onSurface.withValues(alpha: 0.3),
),
),
);
@@ -1006,6 +1024,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
/// 构建图片项
Widget _buildImageItem(int index) {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: () => _showImagePreview(index),
onLongPress: () => _showDeleteImageDialog(index),
@@ -1015,7 +1034,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
height: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outline, width: 0.5),
),
clipBehavior: Clip.antiAlias,
child: Image.file(
@@ -1056,8 +1075,10 @@ class _NoteFormPageState extends State<NoteFormPage> {
void _showDeleteImageDialog(int index) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
builder: (context) {
final colors = Theme.of(context).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text(
@@ -1067,11 +1088,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
fontWeight: FontWeight.w600,
),
),
content: const Text(
content: Text(
'确定要删除这张图片吗?此操作不可恢复。',
style: TextStyle(
fontSize: 14,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.5,
),
),
@@ -1079,7 +1100,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF666666),
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('取消'),
@@ -1090,8 +1111,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
backgroundColor: colors.error,
foregroundColor: colors.onError,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
@@ -1102,7 +1123,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
),
],
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
);
},
);
}
}

View File

@@ -26,21 +26,22 @@ class _NoteSharePageState extends State<NoteSharePage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
backgroundColor: colors.surfaceContainerHighest,
appBar: AppBar(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)),
icon: Icon(Icons.close, color: colors.onSurface),
onPressed: () => Navigator.pop(context),
),
title: const Text(
title: Text(
'分享笔记',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
centerTitle: true,
@@ -53,12 +54,12 @@ class _NoteSharePageState extends State<NoteSharePage> {
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text(
: Text(
'分享',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
),
@@ -78,6 +79,7 @@ class _NoteSharePageState extends State<NoteSharePage> {
}
Widget _buildPosterWidget() {
final colors = Theme.of(context).colorScheme;
final note = widget.note;
final hasImages = note.images.isNotEmpty;
final dateStr =
@@ -88,7 +90,7 @@ class _NoteSharePageState extends State<NoteSharePage> {
return Container(
width: 320,
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
@@ -124,10 +126,10 @@ class _NoteSharePageState extends State<NoteSharePage> {
if (note.title.isNotEmpty) ...[
Text(
note.title,
style: const TextStyle(
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
const SizedBox(height: 4),
@@ -136,9 +138,9 @@ class _NoteSharePageState extends State<NoteSharePage> {
// 日期
Text(
dateStr,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
@@ -152,12 +154,12 @@ class _NoteSharePageState extends State<NoteSharePage> {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFFF0F0F0),
color: colors.outlineVariant,
borderRadius: BorderRadius.circular(4),
),
child: Text(
tag,
style: const TextStyle(fontSize: 11, color: Color(0xFF888888)),
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.5)),
),
);
}).toList(),
@@ -165,7 +167,7 @@ class _NoteSharePageState extends State<NoteSharePage> {
],
const SizedBox(height: 16),
Container(height: 0.5, color: const Color(0xFFE8E8E8)),
Container(height: 0.5, color: colors.outline),
const SizedBox(height: 16),
// 正文内容
@@ -173,9 +175,9 @@ class _NoteSharePageState extends State<NoteSharePage> {
note.content,
maxLines: 12,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF333333),
color: colors.onSurface.withValues(alpha: 0.75),
height: 1.8,
),
),
@@ -191,11 +193,11 @@ class _NoteSharePageState extends State<NoteSharePage> {
],
const Spacer(),
// Mooknote 品牌
const Text(
Text(
'Mooknote',
style: TextStyle(
fontSize: 11,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
fontWeight: FontWeight.w500,
),
),
@@ -210,14 +212,15 @@ class _NoteSharePageState extends State<NoteSharePage> {
}
Widget _buildMetaChip(IconData icon, String text) {
final colors = Theme.of(context).colorScheme;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 13, color: const Color(0xFFAAAAAA)),
Icon(icon, size: 13, color: colors.onSurface.withValues(alpha: 0.35)),
const SizedBox(width: 4),
Text(
text,
style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)),
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35)),
),
],
);

View File

@@ -23,7 +23,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
bool _hasMore = true;
final ScrollController _scrollController = ScrollController();
int _layoutStyle = 0; // 0: 列表, 1: 瀑布流, 2: 时间线
int _layoutStyle = 0;
bool _firstLoad = true;
@override
@@ -110,7 +110,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
Widget build(BuildContext context) {
return Column(
children: [
// 笔记内容
Expanded(
child: Consumer<AppProvider>(
builder: (context, provider, child) {
@@ -122,10 +121,11 @@ class _NoteTabPageState extends State<NoteTabPage> {
}
if (allNotes.isEmpty && _displayedNotes.isEmpty) {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator(
onRefresh: _refresh,
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
color: colors.primary,
backgroundColor: colors.surface,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [_buildEmptyState(context)],
@@ -159,6 +159,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
}
Widget _buildWaterfallSkeleton() {
final colors = Theme.of(context).colorScheme;
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
child: Row(
@@ -168,7 +169,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
children: List.generate(4, (_) => Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 6, offset: const Offset(0, 2)),
@@ -200,25 +201,24 @@ class _NoteTabPageState extends State<NoteTabPage> {
);
}
// ─── 列表视图 ────────────────────────────────────────────────────────
Widget _buildListView() {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator(
onRefresh: _refresh,
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
color: colors.primary,
backgroundColor: colors.surface,
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.fromLTRB(12, 10, 12, 100),
itemCount: _displayedNotes.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= _displayedNotes.length) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: SizedBox(
width: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A)),
child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary),
),
),
);
@@ -229,24 +229,23 @@ class _NoteTabPageState extends State<NoteTabPage> {
);
}
// ─── 时间线视图 ──────────────────────────────────────────────────────
Widget _buildTimelineView() {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator(
onRefresh: _refresh,
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
color: colors.primary,
backgroundColor: colors.surface,
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
itemCount: _displayedNotes.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= _displayedNotes.length) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: SizedBox(width: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A))),
child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)),
),
);
}
@@ -257,6 +256,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
}
Widget _buildTimelineItem(Note note) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: () {
Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) async {
@@ -268,72 +268,60 @@ class _NoteTabPageState extends State<NoteTabPage> {
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 左侧时间线
SizedBox(
width: 40,
child: Column(
children: [
// 圆点
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
border: Border.all(color: colors.surface, width: 2),
),
),
// 连线
Expanded(
child: Container(
width: 1,
color: const Color(0xFFE5E5E5),
color: colors.outline,
),
),
],
),
),
// 右侧内容
Expanded(
child: Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 时间
Text(
_formatFullDate(note.updatedAt),
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)),
),
// 标题
if (note.title.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
note.title,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
// 内容预览
const SizedBox(height: 6),
Text(
_getPreviewText(note),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 13, color: Color(0xFF888888), height: 1.5),
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5), height: 1.5),
),
// 标签
if (note.tags.isNotEmpty) ...[
const SizedBox(height: 8),
Wrap(
@@ -342,10 +330,10 @@ class _NoteTabPageState extends State<NoteTabPage> {
children: note.tags.map((tag) => Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(4),
),
child: Text(tag, style: const TextStyle(fontSize: 10, color: Color(0xFF999999))),
child: Text(tag, style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.4))),
)).toList(),
),
],
@@ -364,10 +352,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
'${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
}
// ─── 瀑布流视图 ──────────────────────────────────────────────────────
Widget _buildWaterfallView() {
// 分为左右两列
final colors = Theme.of(context).colorScheme;
final leftItems = <Note>[];
final rightItems = <Note>[];
for (int i = 0; i < _displayedNotes.length; i++) {
@@ -380,8 +366,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
return RefreshIndicator(
onRefresh: _refresh,
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
color: colors.primary,
backgroundColor: colors.surface,
child: SingleChildScrollView(
controller: _scrollController,
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
@@ -398,6 +384,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
}
Widget _buildWaterfallCard(Note note) {
final colors = Theme.of(context).colorScheme;
final contentText = _getPreviewText(note);
final images = note.images;
final hasImage = images.isNotEmpty;
@@ -413,7 +400,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
child: Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
@@ -428,7 +415,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 顶部图片
if (hasImage)
Stack(
children: [
@@ -458,8 +444,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
),
],
),
// 底部文字区域
Padding(
padding: const EdgeInsets.fromLTRB(10, 8, 10, 10),
child: Column(
@@ -471,47 +455,45 @@ class _NoteTabPageState extends State<NoteTabPage> {
note.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.3,
),
),
if (contentText.isNotEmpty && contentText != '(无内容)') ...[
if (note.title.isNotEmpty) const SizedBox(height: 4),
Text(
contentText,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
style: TextStyle(
fontSize: 11,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
height: 1.4,
),
),
],
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: Text(
_formatTime(note.updatedAt),
style: const TextStyle(fontSize: 10, color: Color(0xFFCCCCCC)),
style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.25)),
),
),
if (note.tags.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(3),
),
child: Text(
note.tags.first,
style: const TextStyle(fontSize: 10, color: Color(0xFF999999)),
style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.4)),
),
),
],
@@ -546,20 +528,21 @@ class _NoteTabPageState extends State<NoteTabPage> {
}
void _showDeleteDialog(BuildContext context, Note note) {
final colors = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
content: const Text('确定要删除这条笔记吗?删除后可在回收站恢复。',
style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)),
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
content: Text('确定要删除这条笔记吗?删除后可在回收站恢复。',
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF666666),
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('取消'),
@@ -570,8 +553,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
backgroundColor: colors.error,
foregroundColor: colors.onError,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@@ -584,8 +567,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
);
}
// ─── 数据同步 ────────────────────────────────────────────────────────
void _syncDisplayedNotes(List<Note> allNotes) {
final validNoteIds = allNotes.map((n) => n.id).toSet();
final initialLength = _displayedNotes.length;
@@ -611,9 +592,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
}
}
// ─── 空状态 ──────────────────────────────────────────────────────────
Widget _buildEmptyState(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -621,24 +601,24 @@ class _NoteTabPageState extends State<NoteTabPage> {
Container(
width: 80, height: 80,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(Icons.note_outlined, size: 40, color: Color(0xFFCCCCCC)),
child: Icon(Icons.note_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
),
const SizedBox(height: 20),
const Text('暂无笔记', style: TextStyle(fontSize: 16, color: Color(0xFF999999))),
Text('暂无笔记', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 24),
InkWell(
onTap: () => Navigator.pushNamed(context, '/note-form'),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(8),
),
child: const Text('添加记录',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
child: Text('添加记录',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)),
),
),
],

View File

@@ -51,12 +51,13 @@ class _ProfilePageState extends State<ProfilePage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Column(
children: [
AppBar(
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.menu, color: Color(0xFF1A1A1A)),
icon: Icon(Icons.menu, color: colors.onSurface),
onPressed: () => Scaffold.of(context).openDrawer(),
),
),
@@ -64,7 +65,7 @@ class _ProfilePageState extends State<ProfilePage> {
),
Expanded(
child: Container(
color: const Color(0xFFF8F8F8),
color: colors.surfaceContainerHigh,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -98,6 +99,7 @@ class _ProfilePageState extends State<ProfilePage> {
Widget _buildUserCard() {
return Consumer<AppProvider>(
builder: (context, provider, child) {
final colors = Theme.of(context).colorScheme;
final movieCount = provider.movies.where((m) => !m.isDeleted).length;
final bookCount = provider.books.length;
final noteCount = provider.notes.length;
@@ -106,7 +108,7 @@ class _ProfilePageState extends State<ProfilePage> {
margin: const EdgeInsets.fromLTRB(16, 12, 16, 0),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)),
@@ -122,14 +124,14 @@ class _ProfilePageState extends State<ProfilePage> {
width: 64, height: 64,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFF5F5F5),
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5),
color: colors.surfaceContainerHighest,
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
clipBehavior: Clip.antiAlias,
child: _avatarPath != null && _avatarPath!.isNotEmpty
? Image.file(File(_avatarPath!), fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Icon(Icons.person_outline, size: 32, color: Color(0xFFCCCCCC)))
: const Icon(Icons.person_outline, size: 32, color: Color(0xFFCCCCCC)),
errorBuilder: (_, __, ___) => Icon(Icons.person_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.25)))
: Icon(Icons.person_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.25)),
),
),
const SizedBox(width: 16),
@@ -139,25 +141,25 @@ class _ProfilePageState extends State<ProfilePage> {
children: [
GestureDetector(
onTap: () => _editNickname(context),
child: Text(_nickname, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
child: Text(_nickname, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: colors.onSurface)),
),
const SizedBox(height: 4),
GestureDetector(
onTap: () => _editMotto(context),
child: Text(_motto, maxLines: 1, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 13, color: Color(0xFFAAAAAA))),
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.35))),
),
],
),
),
IconButton(
icon: const Icon(Icons.settings_outlined, color: Color(0xFF999999)),
icon: Icon(Icons.settings_outlined, color: colors.onSurface.withValues(alpha: 0.4)),
onPressed: () => _showSettings(context),
),
],
),
const SizedBox(height: 16),
const Divider(height: 1, color: Color(0xFFF0F0F0)),
Divider(height: 1, color: colors.outlineVariant),
const SizedBox(height: 12),
_buildStatRow(Icons.movie_outlined, _formatCount(movieCount), '观影'),
const SizedBox(height: 8),
@@ -172,13 +174,14 @@ class _ProfilePageState extends State<ProfilePage> {
}
Widget _buildStatRow(IconData icon, String count, String label) {
final colors = Theme.of(context).colorScheme;
return Row(
children: [
Icon(icon, size: 14, color: const Color(0xFFBBBBBB)),
Icon(icon, size: 14, color: colors.onSurface.withValues(alpha: 0.3)),
const SizedBox(width: 8),
Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
Text(label, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
const Spacer(),
Text(count, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
Text(count, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
],
);
}
@@ -192,11 +195,12 @@ class _ProfilePageState extends State<ProfilePage> {
// ─── 探索行 ──────────────────────────────────────────────────────────
Widget _buildExploreRow() {
final colors = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)),
@@ -204,9 +208,9 @@ class _ProfilePageState extends State<ProfilePage> {
),
child: Row(
children: [
const Icon(Icons.auto_awesome, size: 16, color: Color(0xFFBBBBBB)),
Icon(Icons.auto_awesome, size: 16, color: colors.onSurface.withValues(alpha: 0.3)),
const SizedBox(width: 10),
const Text('快捷入口', style: TextStyle(fontSize: 13, color: Color(0xFF888888))),
Text('快捷入口', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
const Spacer(),
_buildQuickAction(Icons.explore_outlined, '漫步', () => Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage()))),
const SizedBox(width: 24),
@@ -217,14 +221,15 @@ class _ProfilePageState extends State<ProfilePage> {
}
Widget _buildQuickAction(IconData icon, String label, VoidCallback onTap) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 20, color: const Color(0xFF555555)),
Icon(icon, size: 20, color: colors.onSurface.withValues(alpha: 0.7)),
const SizedBox(height: 2),
Text(label, style: const TextStyle(fontSize: 10, color: Color(0xFF999999))),
Text(label, style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.4))),
],
),
);
@@ -233,17 +238,19 @@ class _ProfilePageState extends State<ProfilePage> {
// ─── 菜单 ────────────────────────────────────────────────────────────
Widget _buildSectionHeader(String title) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 10),
child: Text(title, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFFBBBBBB))),
child: Text(title, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.3))),
);
}
Widget _buildMenuGroup(List<_MenuEntry> entries) {
final colors = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)),
@@ -269,19 +276,19 @@ class _ProfilePageState extends State<ProfilePage> {
Container(
width: 36, height: 36,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Icon(e.value.icon, size: 18, color: const Color(0xFF666666)),
child: Icon(e.value.icon, size: 18, color: colors.onSurface.withValues(alpha: 0.6)),
),
const SizedBox(width: 12),
Expanded(child: Text(e.value.title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))),
const Icon(Icons.chevron_right, size: 16, color: Color(0xFFD0D0D0)),
Expanded(child: Text(e.value.title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface))),
Icon(Icons.chevron_right, size: 16, color: colors.onSurface.withValues(alpha: 0.2)),
],
),
),
),
if (!isLast) const Divider(height: 1, indent: 68, endIndent: 20, color: Color(0xFFF0F0F0)),
if (!isLast) Divider(height: 1, indent: 68, endIndent: 20, color: colors.outlineVariant),
],
);
}).toList(),
@@ -310,16 +317,17 @@ class _ProfilePageState extends State<ProfilePage> {
}
void _editNickname(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final controller = TextEditingController(text: _nickname);
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white, elevation: 0,
backgroundColor: colors.surface, 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))))),
title: Text('修改昵称', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
content: TextField(controller: controller, decoration: InputDecoration(hintText: '输入昵称', border: UnderlineInputBorder(borderSide: BorderSide(color: colors.outline)))),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消', style: TextStyle(color: Color(0xFF666666)))),
TextButton(onPressed: () => Navigator.pop(context), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
TextButton(onPressed: () async {
final newNickname = controller.text.trim();
if (newNickname.isNotEmpty) {
@@ -334,16 +342,17 @@ class _ProfilePageState extends State<ProfilePage> {
}
void _editMotto(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final controller = TextEditingController(text: _motto);
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white, elevation: 0,
backgroundColor: colors.surface, 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))))),
title: Text('修改座右铭', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
content: TextField(controller: controller, maxLines: 2, decoration: InputDecoration(hintText: '输入座右铭', border: UnderlineInputBorder(borderSide: BorderSide(color: colors.outline)))),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消', style: TextStyle(color: Color(0xFF666666)))),
TextButton(onPressed: () => Navigator.pop(context), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
TextButton(onPressed: () async {
final newMotto = controller.text.trim();
await _userPrefs.setMotto(newMotto);
@@ -358,34 +367,35 @@ class _ProfilePageState extends State<ProfilePage> {
// ─── 备份弹窗 ────────────────────────────────────────────────────────
void _showBackupOptions(BuildContext context) {
final colors = Theme.of(context).colorScheme;
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
backgroundColor: colors.surface,
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))),
Container(width: 36, height: 4, decoration: BoxDecoration(color: colors.onSurface.withValues(alpha: 0.15), 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)))),
Align(alignment: Alignment.centerLeft, child: Text('选择备份方式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface))),
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)),
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.folder_outlined, color: colors.onSurface.withValues(alpha: 0.6))),
title: Text('本地备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: Text('备份到本地文件夹,支持恢复', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)),
onTap: () { Navigator.pop(ctx); _push(context, const BackupPage()); },
),
const Divider(height: 0.5, color: Color(0xFFF0F0F0)),
Divider(height: 0.5, color: colors.outlineVariant),
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)),
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.cloud_outlined, color: colors.onSurface.withValues(alpha: 0.6))),
title: Text('云备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: Text('通过 WebDAV 同步到云端', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)),
onTap: () { Navigator.pop(ctx); _push(context, const CloudSyncPage()); },
),
const SizedBox(height: 20),
@@ -421,17 +431,20 @@ class SettingsPage extends StatefulWidget {
class _SettingsPageState extends State<SettingsPage> {
final UserPrefs _userPrefs = UserPrefs();
bool _hideBottomNavOnScroll = true;
int _themeMode = 0; // 0=系统, 1=浅色, 2=深色
@override
void initState() {
super.initState();
_hideBottomNavOnScroll = _userPrefs.hideBottomNavOnScroll;
_themeMode = _userPrefs.themeMode;
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(title: const Text('设置')),
body: ListView(
children: [
@@ -442,7 +455,7 @@ class _SettingsPageState extends State<SettingsPage> {
subtitle: '清理未在数据库中引用的图片文件',
onTap: () => _showClearCacheDialog(context),
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildSwitchItem(
icon: Icons.swipe_vertical_outlined,
title: '底部导航栏滚动隐藏',
@@ -450,28 +463,31 @@ class _SettingsPageState extends State<SettingsPage> {
value: _hideBottomNavOnScroll,
onChanged: _toggleHideBottomNavOnScroll,
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildNavigationItem(
icon: Icons.apps_outlined,
title: '应用图标',
subtitle: '更换桌面应用图标',
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AppIconPickerPage())),
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildNavigationItem(
icon: Icons.view_list_outlined,
title: '主界面设置',
subtitle: '启动标签、模块显示开关',
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MainContentSettingsPage())),
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildNavigationItem(
icon: Icons.dashboard_outlined,
title: '布局设置',
subtitle: '笔记、影视、阅读的展示样式',
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const LayoutSettingsPage())),
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildSectionHeader('外观'),
_buildThemeModeSelector(),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildSectionHeader('帮助'),
_buildLinkItem(
context: context,
@@ -480,7 +496,7 @@ class _SettingsPageState extends State<SettingsPage> {
subtitle: '查看应用使用指南',
url: 'https://mooknote.iletter.top/#/guide',
),
const Divider(height: 0.5, indent: 24, endIndent: 24),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
],
),
);
@@ -491,23 +507,93 @@ class _SettingsPageState extends State<SettingsPage> {
setState(() => _hideBottomNavOnScroll = value);
}
static const _themeModeLabels = ['跟随系统', '浅色模式', '深色模式'];
static const _themeModeIcons = [Icons.brightness_auto, Icons.light_mode, Icons.dark_mode];
Widget _buildThemeModeSelector() {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: () => _showThemeModePicker(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row(
children: [
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(_themeModeIcons[_themeMode], color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
const SizedBox(width: 16),
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('主题模式', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
const SizedBox(height: 2),
Text(_themeModeLabels[_themeMode], style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
]),
),
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20),
],
),
),
);
}
void _showThemeModePicker() {
final colors = Theme.of(context).colorScheme;
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (ctx) => Container(
decoration: BoxDecoration(color: colors.surface, borderRadius: const 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: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
const SizedBox(height: 20),
Align(alignment: Alignment.centerLeft, child: Padding(padding: const EdgeInsets.symmetric(horizontal: 24), child: Text('主题模式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)))),
const SizedBox(height: 16),
for (int i = 0; i < _themeModeLabels.length; i++)
ListTile(
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(_themeModeIcons[i], color: colors.onSurface.withValues(alpha: 0.6))),
title: Text(_themeModeLabels[i], style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
trailing: _themeMode == i ? Icon(Icons.check, color: colors.onSurface, size: 20) : null,
onTap: () async { await _setThemeMode(i); Navigator.pop(ctx); },
),
const SizedBox(height: 8),
],
),
),
);
}
Future<void> _setThemeMode(int mode) async {
await _userPrefs.setThemeMode(mode);
final themeMode = switch (mode) {
1 => ThemeMode.light,
2 => ThemeMode.dark,
_ => ThemeMode.system,
};
if (mounted) {
context.read<AppProvider>().setThemeMode(themeMode);
setState(() => _themeMode = mode);
}
}
Widget _buildSwitchItem({required IconData icon, required String title, required String subtitle, required bool value, required ValueChanged<bool> onChanged}) {
final colors = Theme.of(context).colorScheme;
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)),
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), 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))),
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
const SizedBox(height: 2),
Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
]),
),
Switch(value: value, onChanged: onChanged, activeColor: const Color(0xFF1A1A1A), activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), inactiveThumbColor: Colors.white, inactiveTrackColor: const Color(0xFFE5E5E5)),
Switch(value: value, onChanged: onChanged, activeColor: colors.primary, activeTrackColor: colors.primary.withOpacity(0.3), inactiveThumbColor: colors.surface, inactiveTrackColor: colors.outline),
],
),
),
@@ -515,29 +601,31 @@ class _SettingsPageState extends State<SettingsPage> {
}
Widget _buildSectionHeader(String title) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.fromLTRB(24, 32, 24, 12),
child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
);
}
Widget _buildNavigationItem({required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
final colors = Theme.of(context).colorScheme;
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)),
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), 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))),
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
const SizedBox(height: 2),
Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
]),
),
const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC), size: 20),
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20),
],
),
),
@@ -545,22 +633,23 @@ class _SettingsPageState extends State<SettingsPage> {
}
Widget _buildLinkItem({required BuildContext context, required IconData icon, required String title, required String subtitle, required String url}) {
final colors = Theme.of(context).colorScheme;
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)),
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), 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))),
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
const SizedBox(height: 2),
Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
]),
),
const Icon(Icons.open_in_new, color: Color(0xFFCCCCCC), size: 18),
Icon(Icons.open_in_new, color: colors.onSurface.withValues(alpha: 0.25), size: 18),
],
),
),
@@ -568,19 +657,20 @@ class _SettingsPageState extends State<SettingsPage> {
}
Widget _buildActionItem({required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
final colors = Theme.of(context).colorScheme;
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)),
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), 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))),
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
const SizedBox(height: 2),
Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
]),
),
],
@@ -590,19 +680,20 @@ class _SettingsPageState extends State<SettingsPage> {
}
void _showClearCacheDialog(BuildContext pageContext) {
final colors = Theme.of(context).colorScheme;
showDialog(
context: pageContext,
builder: (dialogContext) => AlertDialog(
backgroundColor: Colors.white, elevation: 0,
backgroundColor: colors.surface, 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: () => Navigator.pop(dialogContext), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
TextButton(onPressed: () async {
Navigator.pop(dialogContext);
await _clearCacheData(pageContext);
}, child: const Text('确定', style: TextStyle(color: Colors.red))),
}, child: Text('确定', style: TextStyle(color: colors.error))),
],
),
);
@@ -712,24 +803,25 @@ class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(title: const Text('主界面设置')),
body: ListView(
children: [
_buildSectionHeader('启动设置'),
_buildDefaultTabSelector(),
const Divider(height: 0.5, indent: 24, endIndent: 24),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildSectionHeader('模块开关'),
_buildSwitchItem(Icons.movie_outlined, '观影', '记录和管理观影记录', _showMovieTab, _toggleMovieTab),
const Divider(height: 0.5, indent: 24, endIndent: 24),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildSwitchItem(Icons.menu_book_outlined, '阅读', '记录和管理阅读记录', _showBookTab, _toggleBookTab),
const Divider(height: 0.5, indent: 24, endIndent: 24),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildSwitchItem(Icons.note_outlined, '笔记', '记录和管理笔记', _showNoteTab, _toggleNoteTab),
const Divider(height: 0.5, indent: 24, endIndent: 24),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
child: const Text('至少保留一个模块,关闭后对应标签页将不再显示。', style: TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))),
child: Text('至少保留一个模块,关闭后对应标签页将不再显示。', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))),
),
],
),
@@ -737,37 +829,40 @@ class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
}
Widget _buildSectionHeader(String title) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12),
child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
);
}
Widget _buildSwitchItem(IconData icon, String title, String subtitle, bool value, ValueChanged<bool> onChanged) {
final colors = Theme.of(context).colorScheme;
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: Switch(value: value, onChanged: onChanged, activeColor: const Color(0xFF1A1A1A), activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), inactiveThumbColor: Colors.white, inactiveTrackColor: const Color(0xFFE5E5E5)),
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
title: Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
trailing: Switch(value: value, onChanged: onChanged, activeColor: colors.primary, activeTrackColor: colors.primary.withOpacity(0.3), inactiveThumbColor: colors.surface, inactiveTrackColor: colors.outline),
);
}
Widget _buildDefaultTabSelector() {
final colors = Theme.of(context).colorScheme;
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))),
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.home_outlined, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
title: Text('默认启动标签', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: Text('打开应用时默认显示的页面', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(labels[_defaultTabIndex], style: const TextStyle(fontSize: 14, color: Color(0xFF999999))),
Text(labels[_defaultTabIndex], style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(width: 4),
const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC), size: 20),
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20),
],
),
onTap: () => _showDefaultTabPicker(labels, icons),
@@ -775,24 +870,25 @@ class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
}
void _showDefaultTabPicker(List<String> labels, List<IconData> icons) {
final colors = Theme.of(context).colorScheme;
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (ctx) => Container(
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
decoration: BoxDecoration(color: colors.surface, borderRadius: const 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))),
Container(width: 36, height: 4, decoration: BoxDecoration(color: colors.onSurface.withValues(alpha: 0.15), 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))))),
Align(alignment: Alignment.centerLeft, child: Padding(padding: const EdgeInsets.symmetric(horizontal: 24), child: Text('默认启动标签', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)))),
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,
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icons[i], color: colors.onSurface.withValues(alpha: 0.6))),
title: Text(labels[i], style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
trailing: _defaultTabIndex == i ? Icon(Icons.check, color: colors.onSurface, size: 20) : null,
onTap: () async { await _userPrefs.setDefaultMainTabIndex(i); setState(() => _defaultTabIndex = i); Navigator.pop(ctx); },
),
const SizedBox(height: 8),
@@ -828,28 +924,29 @@ class _LayoutSettingsPageState extends State<LayoutSettingsPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
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),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_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),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildLayoutOption(icon: Icons.timeline_outlined, title: '时间线布局', subtitle: '按时间排列,纵览全局', value: 2, groupValue: _noteLayout, onTap: () => _setLayout('note', 2)),
const Divider(height: 0.5, indent: 24, endIndent: 24),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_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),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_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),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_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),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_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),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
],
),
);
@@ -864,20 +961,22 @@ class _LayoutSettingsPageState extends State<LayoutSettingsPage> {
}
Widget _buildSectionHeader(String title) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12),
child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
);
}
Widget _buildLayoutOption({required IconData icon, required String title, required String subtitle, required int value, required int groupValue, required VoidCallback onTap}) {
final colors = Theme.of(context).colorScheme;
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),
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
title: Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
trailing: selected ? Icon(Icons.check_circle, color: colors.onSurface, size: 20) : Icon(Icons.circle_outlined, color: colors.onSurface.withValues(alpha: 0.15), size: 20),
onTap: onTap,
);
}
@@ -911,10 +1010,11 @@ class _WebViewPageState extends State<WebViewPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
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)))]),
body: Stack(children: [WebViewWidget(controller: _controller), if (_isLoading) Center(child: CircularProgressIndicator(color: colors.onSurface.withValues(alpha: 0.4)))]),
);
}
}

View File

@@ -49,7 +49,7 @@ class _DeletedItem {
class _RecycleBinPageState extends State<RecycleBinPage> {
List<_DeletedItem> _allItems = [];
_ItemType? _filterType; // null = 全部
_ItemType? _filterType;
bool _isLoading = true;
List<_DeletedItem> get _filteredItems =>
@@ -80,8 +80,9 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: const Text('回收站'),
actions: [
@@ -91,12 +92,12 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
child: TextButton(
onPressed: _showClearAllDialog,
style: TextButton.styleFrom(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: const BorderSide(color: Color(0xFFFFDDDD), width: 0.5),
side: BorderSide(color: colors.error.withValues(alpha: 0.15), width: 0.5),
),
minimumSize: Size.zero,
),
@@ -106,12 +107,10 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A)))
? Center(child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary))
: Column(
children: [
// 筛选标签行
if (_allItems.isNotEmpty) _buildFilterRow(),
// 列表
Expanded(
child: _filteredItems.isEmpty
? _buildEmptyState()
@@ -130,11 +129,12 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
}
Widget _buildFilterRow() {
final colors = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: Color(0xFFEEEEEE), width: 0.5)),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5)),
),
child: Wrap(
spacing: 8,
@@ -149,6 +149,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
}
Widget _filterChip(String label, _ItemType? type) {
final colors = Theme.of(context).colorScheme;
final active = _filterType == type;
final count = type == null ? _allItems.length : _allItems.where((i) => i.type == type).length;
return GestureDetector(
@@ -156,7 +157,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration(
color: active ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5),
color: active ? colors.primary : colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
),
child: Text(
@@ -164,7 +165,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: active ? Colors.white : const Color(0xFF888888),
color: active ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.5),
),
),
),
@@ -172,6 +173,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
}
Widget _buildCard(_DeletedItem item) {
final colors = Theme.of(context).colorScheme;
return Dismissible(
key: Key('${item.type.name}_${item.id}'),
direction: DismissDirection.endToStart,
@@ -181,9 +183,9 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
@@ -193,11 +195,11 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Icon(item.icon, size: 20, color: const Color(0xFF888888)),
child: Icon(item.icon, size: 20, color: colors.onSurface.withValues(alpha: 0.5)),
),
const SizedBox(width: 12),
Expanded(
@@ -210,7 +212,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
Expanded(
child: Text(
item.title,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A), height: 1.3),
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface, height: 1.3),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -219,13 +221,13 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(3),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Text(
item.typeLabel,
style: const TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: Color(0xFF999999)),
style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.4)),
),
),
],
@@ -233,15 +235,15 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
const SizedBox(height: 3),
Text(
item.subtitle,
style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)),
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35)),
),
],
),
),
const SizedBox(width: 8),
_actionBtn(Icons.restore, '恢复', const Color(0xFF1A1A1A), () => _restore(item)),
_actionBtn(Icons.restore, '恢复', colors.primary, () => _restore(item)),
const SizedBox(width: 6),
_actionBtn(Icons.delete_outline, '删除', Colors.red, () => _permanentDelete(item)),
_actionBtn(Icons.delete_outline, '删除', colors.error, () => _permanentDelete(item)),
],
),
),
@@ -263,6 +265,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
}
Widget _actionBtn(IconData icon, String tooltip, Color color, VoidCallback onTap) {
final colors = Theme.of(context).colorScheme;
return Material(
color: Colors.transparent,
child: InkWell(
@@ -274,9 +277,9 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Icon(icon, size: 16, color: color),
),
@@ -286,6 +289,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
}
Widget _buildEmptyState() {
final colors = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -294,18 +298,18 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
width: 72,
height: 72,
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(18),
),
child: const Icon(Icons.delete_outline, size: 32, color: Color(0xFFD5D5D5)),
child: Icon(Icons.delete_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.15)),
),
const SizedBox(height: 16),
Text(
_filterType == null ? '回收站是空的' : '没有删除的项目',
style: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)),
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
),
const SizedBox(height: 4),
const Text('删除的项目会显示在这里', style: TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))),
Text('删除的项目会显示在这里', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.25))),
],
),
);
@@ -344,25 +348,26 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
}
Future<bool> _showConfirmDialog(String message) async {
final colors = Theme.of(context).colorScheme;
final result = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
content: Text(message, style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)),
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
content: Text(message, style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
style: TextButton.styleFrom(foregroundColor: const Color(0xFF666666)),
style: TextButton.styleFrom(foregroundColor: colors.onSurface.withValues(alpha: 0.6)),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
backgroundColor: colors.error,
foregroundColor: colors.onError,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
@@ -377,27 +382,28 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
void _showClearAllDialog() {
final pageContext = context;
final colors = Theme.of(pageContext).colorScheme;
showDialog(
context: pageContext,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Row(
title: Row(
children: [
Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
SizedBox(width: 8),
Text('清空回收站', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
const SizedBox(width: 8),
Text('清空回收站', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
],
),
content: const Text(
content: Text(
'确定要清空回收站吗?所有项目将被彻底删除,此操作不可恢复。',
style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5),
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
style: TextButton.styleFrom(foregroundColor: const Color(0xFF666666)),
style: TextButton.styleFrom(foregroundColor: colors.onSurface.withValues(alpha: 0.6)),
child: const Text('取消'),
),
ElevatedButton(
@@ -408,8 +414,8 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
if (mounted) ToastUtil.show(pageContext, '回收站已清空');
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
backgroundColor: colors.error,
foregroundColor: colors.onError,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),

View File

@@ -100,8 +100,9 @@ class _SearchPageState extends State<SearchPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: const Text('搜索'),
elevation: 0,
@@ -121,18 +122,19 @@ class _SearchPageState extends State<SearchPage> {
}
Widget _buildSearchBar() {
final colors = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 4),
child: TextField(
controller: _searchController,
focusNode: _focusNode,
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
style: TextStyle(fontSize: 15, color: colors.onSurface),
decoration: InputDecoration(
hintText: '搜索标题、别名、内容...',
hintStyle: const TextStyle(color: Color(0xFFB0B0B0), fontSize: 15),
prefixIcon: const Padding(
padding: EdgeInsets.only(left: 12, right: 8),
child: Icon(Icons.search, color: Color(0xFF1A1A1A), size: 22),
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.35), fontSize: 15),
prefixIcon: Padding(
padding: const EdgeInsets.only(left: 12, right: 8),
child: Icon(Icons.search, color: colors.onSurface, size: 22),
),
prefixIconConstraints: const BoxConstraints(minWidth: 42, minHeight: 42),
suffixIcon: _searchController.text.isNotEmpty
@@ -147,15 +149,15 @@ class _SearchPageState extends State<SearchPage> {
width: 28,
height: 28,
decoration: BoxDecoration(
color: const Color(0xFFE5E5E5),
color: colors.outline,
borderRadius: BorderRadius.circular(14),
),
child: const Icon(Icons.close, color: Color(0xFF666666), size: 16),
child: Icon(Icons.close, color: colors.onSurface.withValues(alpha: 0.6), size: 16),
),
)
: null,
filled: true,
fillColor: const Color(0xFFF8F8F8),
fillColor: colors.surfaceContainerHigh,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
@@ -166,7 +168,7 @@ class _SearchPageState extends State<SearchPage> {
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1.5),
borderSide: BorderSide(color: colors.primary, width: 1.5),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
@@ -205,26 +207,27 @@ class _SearchPageState extends State<SearchPage> {
}
Widget _buildTypeChip(String label, IconData icon, bool selected, ValueChanged<bool> onChanged) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: () => onChanged(!selected),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
decoration: BoxDecoration(
color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5),
color: selected ? colors.primary : colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: selected ? Colors.white : const Color(0xFF888888)),
Icon(icon, size: 14, color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.5)),
const SizedBox(width: 5),
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: selected ? Colors.white : const Color(0xFF888888),
color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.5),
),
),
],
@@ -234,6 +237,7 @@ class _SearchPageState extends State<SearchPage> {
}
Widget _buildInitialState() {
final colors = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -242,21 +246,22 @@ class _SearchPageState extends State<SearchPage> {
width: 88,
height: 88,
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
),
child: const Icon(Icons.search_rounded, size: 44, color: Color(0xFFD0D0D0)),
child: Icon(Icons.search_rounded, size: 44, color: colors.onSurface.withValues(alpha: 0.2)),
),
const SizedBox(height: 24),
const Text('输入关键词搜索', style: TextStyle(fontSize: 15, color: Color(0xFF999999))),
Text('输入关键词搜索', style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 6),
const Text('可同时筛选影视、书籍、笔记', style: TextStyle(fontSize: 13, color: Color(0xFFCCCCCC))),
Text('可同时筛选影视、书籍、笔记', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.25))),
],
),
);
}
Widget _buildEmptyState() {
final colors = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -265,15 +270,15 @@ class _SearchPageState extends State<SearchPage> {
width: 88,
height: 88,
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
),
child: const Icon(Icons.search_off_rounded, size: 44, color: Color(0xFFD0D0D0)),
child: Icon(Icons.search_off_rounded, size: 44, color: colors.onSurface.withValues(alpha: 0.2)),
),
const SizedBox(height: 24),
const Text('未找到相关内容', style: TextStyle(fontSize: 15, color: Color(0xFF999999))),
Text('未找到相关内容', style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 6),
const Text('换个关键词试试', style: TextStyle(fontSize: 13, color: Color(0xFFCCCCCC))),
Text('换个关键词试试', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.25))),
],
),
);
@@ -302,13 +307,14 @@ class _SearchPageState extends State<SearchPage> {
// ─── 影视结果项 ──────────────────────────────────────────────────────
Widget _buildMovieItem(Movie movie) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => MovieDetailPage(movie: movie))),
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(14),
),
child: Row(
@@ -328,17 +334,17 @@ class _SearchPageState extends State<SearchPage> {
),
const SizedBox(height: 8),
Text(movie.title, maxLines: 1, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
if (movie.alternateTitles.isNotEmpty) ...[
const SizedBox(height: 3),
Text(movie.alternateTitles.take(2).join(''), maxLines: 1, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
],
],
),
),
const SizedBox(width: 8),
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
],
),
),
@@ -348,13 +354,14 @@ class _SearchPageState extends State<SearchPage> {
// ─── 书籍结果项 ──────────────────────────────────────────────────────
Widget _buildBookItem(Book book) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => BookDetailPage(book: book))),
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(14),
),
child: Row(
@@ -374,17 +381,17 @@ class _SearchPageState extends State<SearchPage> {
),
const SizedBox(height: 8),
Text(book.title, maxLines: 1, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
if (book.authors.isNotEmpty) ...[
const SizedBox(height: 3),
Text(book.authors.take(2).join(''), maxLines: 1, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
],
],
),
),
const SizedBox(width: 8),
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
],
),
),
@@ -394,13 +401,14 @@ class _SearchPageState extends State<SearchPage> {
// ─── 笔记结果项 ──────────────────────────────────────────────────────
Widget _buildNoteItem(Note note) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => NoteDetailPage(note: note))),
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(14),
),
child: Column(
@@ -410,7 +418,7 @@ class _SearchPageState extends State<SearchPage> {
children: [
_typeBadge('笔记', const Color(0xFF66BB6A)),
const Spacer(),
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
],
),
const SizedBox(height: 10),
@@ -418,7 +426,7 @@ class _SearchPageState extends State<SearchPage> {
note.summary.trim().isEmpty ? '(无内容)' : note.summary.trim(),
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14, color: Color(0xFF333333), height: 1.6),
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75), height: 1.6),
),
if (note.tags.isNotEmpty) ...[
const SizedBox(height: 10),
@@ -428,10 +436,10 @@ class _SearchPageState extends State<SearchPage> {
children: note.tags.map((tag) => Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(6),
),
child: Text(tag, style: const TextStyle(fontSize: 11, color: Color(0xFF888888))),
child: Text(tag, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.5))),
)).toList(),
),
],
@@ -444,18 +452,19 @@ class _SearchPageState extends State<SearchPage> {
// ─── 通用组件 ────────────────────────────────────────────────────────
Widget _buildPosterThumb(String? path, IconData fallback) {
final colors = Theme.of(context).colorScheme;
return Container(
width: 48,
height: 64,
decoration: BoxDecoration(
color: const Color(0xFFF0F0F0),
color: colors.outlineVariant,
borderRadius: BorderRadius.circular(8),
),
clipBehavior: Clip.antiAlias,
child: path != null && path.isNotEmpty
? Image.file(File(path), fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Icon(fallback, size: 22, color: const Color(0xFFCCCCCC)))
: Icon(fallback, size: 22, color: const Color(0xFFCCCCCC)),
errorBuilder: (_, __, ___) => Icon(fallback, size: 22, color: colors.onSurface.withValues(alpha: 0.25)))
: Icon(fallback, size: 22, color: colors.onSurface.withValues(alpha: 0.25)),
);
}
@@ -471,11 +480,12 @@ class _SearchPageState extends State<SearchPage> {
}
Widget _statusBadge(String status) {
final colors = Theme.of(context).colorScheme;
final (label, bg, fg) = switch (status) {
'watched' => ('已看', const Color(0xFF1A1A1A), Colors.white),
'watching' => ('在看', const Color(0xFFF0F0F0), const Color(0xFF666666)),
'want_to_watch' => ('想看', const Color(0xFFF5F5F5), const Color(0xFF999999)),
_ => ('', const Color(0xFFF5F5F5), const Color(0xFFBBBBBB)),
'watched' => ('已看', colors.primary, colors.onPrimary),
'watching' => ('在看', colors.outlineVariant, colors.onSurface.withValues(alpha: 0.6)),
'want_to_watch' => ('想看', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.4)),
_ => ('', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.3)),
};
if (label.isEmpty) return const SizedBox.shrink();
return Container(
@@ -486,11 +496,12 @@ class _SearchPageState extends State<SearchPage> {
}
Widget _bookStatusBadge(String status) {
final colors = Theme.of(context).colorScheme;
final (label, bg, fg) = switch (status) {
'read' => ('已读', const Color(0xFF1A1A1A), Colors.white),
'reading' => ('在读', const Color(0xFFF0F0F0), const Color(0xFF666666)),
'want_to_read' => ('想读', const Color(0xFFF5F5F5), const Color(0xFF999999)),
_ => ('', const Color(0xFFF5F5F5), const Color(0xFFBBBBBB)),
'read' => ('已读', colors.primary, colors.onPrimary),
'reading' => ('在读', colors.outlineVariant, colors.onSurface.withValues(alpha: 0.6)),
'want_to_read' => ('想读', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.4)),
_ => ('', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.3)),
};
if (label.isEmpty) return const SizedBox.shrink();
return Container(

View File

@@ -19,8 +19,9 @@ class _StatisticsPageState extends State<StatisticsPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(title: const Text('数据统计')),
body: Consumer<AppProvider>(
builder: (context, provider, child) {
@@ -63,6 +64,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
// ─── 总览卡片 ────────────────────────────────────────────────────────
Widget _buildTotalCards(List<Movie> movies, List<Book> books, List<Note> notes) {
final colors = Theme.of(context).colorScheme;
final items = <_CardData>[];
if (_showMovies) items.add(_CardData('影视', movies.length, Icons.movie_outlined, const Color(0xFF4A90D9)));
if (_showBooks) items.add(_CardData('书籍', books.length, Icons.menu_book_outlined, const Color(0xFF7E57C2)));
@@ -83,7 +85,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
const SizedBox(height: 10),
Text('${d.count}', style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700, color: d.color)),
const SizedBox(height: 2),
Text(d.label, style: const TextStyle(fontSize: 12, color: Color(0xFF888888))),
Text(d.label, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5))),
],
),
),
@@ -94,6 +96,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
// ─── 状态分布 ────────────────────────────────────────────────────────
Widget _buildStatusSection(String title, List items, String Function(dynamic) getStatus, Map<String, String> labels) {
final colors = Theme.of(context).colorScheme;
final active = items.where((i) => !(i is Movie) || !i.isDeleted).toList();
final total = active.length;
@@ -110,11 +113,11 @@ class _StatisticsPageState extends State<StatisticsPage> {
children: [
Row(
children: [
Text(e.key, style: const TextStyle(fontSize: 13, color: Color(0xFF666666))),
Text(e.key, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6))),
const Spacer(),
Text('$count', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
Text('$count', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
const SizedBox(width: 4),
Text('${(pct * 100).toStringAsFixed(0)}%', style: const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))),
Text('${(pct * 100).toStringAsFixed(0)}%', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))),
],
),
const SizedBox(height: 6),
@@ -122,8 +125,8 @@ class _StatisticsPageState extends State<StatisticsPage> {
borderRadius: BorderRadius.circular(3),
child: LinearProgressIndicator(
value: pct,
backgroundColor: const Color(0xFFF0F0F0),
color: const Color(0xFF1A1A1A),
backgroundColor: colors.outlineVariant,
color: colors.primary,
minHeight: 6,
),
),
@@ -138,6 +141,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
// ─── 类型/标签分布 ───────────────────────────────────────────────────
Widget _buildGenreDistribution(String title, List items) {
final colors = Theme.of(context).colorScheme;
final genreCounts = <String, int>{};
for (final item in items) {
for (final genre in (item.genres as List<String>)) {
@@ -161,17 +165,17 @@ class _StatisticsPageState extends State<StatisticsPage> {
children: [
SizedBox(
width: 60,
child: Text(e.key, style: const TextStyle(fontSize: 12, color: Color(0xFF666666)), overflow: TextOverflow.ellipsis),
child: Text(e.key, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.6)), overflow: TextOverflow.ellipsis),
),
const SizedBox(width: 10),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(value: pct, backgroundColor: const Color(0xFFF0F0F0), color: const Color(0xFF1A1A1A), minHeight: 4),
child: LinearProgressIndicator(value: pct, backgroundColor: colors.outlineVariant, color: colors.primary, minHeight: 4),
),
),
const SizedBox(width: 8),
Text('${e.value}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
Text('${e.value}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface)),
],
),
);
@@ -181,6 +185,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
}
Widget _buildNoteTagDistribution(String title, List<Note> notes) {
final colors = Theme.of(context).colorScheme;
final tagCounts = <String, int>{};
for (final note in notes) {
for (final tag in note.tags) {
@@ -204,17 +209,17 @@ class _StatisticsPageState extends State<StatisticsPage> {
children: [
SizedBox(
width: 60,
child: Text(e.key, style: const TextStyle(fontSize: 12, color: Color(0xFF666666)), overflow: TextOverflow.ellipsis),
child: Text(e.key, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.6)), overflow: TextOverflow.ellipsis),
),
const SizedBox(width: 10),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(value: pct, backgroundColor: const Color(0xFFF0F0F0), color: const Color(0xFF1A1A1A), minHeight: 4),
child: LinearProgressIndicator(value: pct, backgroundColor: colors.outlineVariant, color: colors.primary, minHeight: 4),
),
),
const SizedBox(width: 8),
Text('${e.value}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
Text('${e.value}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface)),
],
),
);
@@ -226,6 +231,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
// ─── 评分分布 ────────────────────────────────────────────────────────
Widget _buildRatingDistribution(String title, List<Movie> movies, List<Book> books) {
final colors = Theme.of(context).colorScheme;
final allRatings = <double>[];
for (final m in movies) {
if (m.rating != null) allRatings.add(m.rating!);
@@ -255,10 +261,10 @@ class _StatisticsPageState extends State<StatisticsPage> {
children: [
Row(
children: [
const Text('平均评分', style: TextStyle(fontSize: 13, color: Color(0xFF666666))),
Text('平均评分', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6))),
const Spacer(),
Text(avg.toStringAsFixed(1), style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A))),
const Text(' / 10', style: TextStyle(fontSize: 13, color: Color(0xFFBBBBBB))),
Text(avg.toStringAsFixed(1), style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: colors.onSurface)),
Text(' / 10', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.3))),
],
),
const SizedBox(height: 16),
@@ -268,7 +274,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
children: [
SizedBox(
width: 28,
child: Text(ranges[i], style: const TextStyle(fontSize: 11, color: Color(0xFF999999))),
child: Text(ranges[i], style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
),
const SizedBox(width: 8),
Expanded(
@@ -276,14 +282,14 @@ class _StatisticsPageState extends State<StatisticsPage> {
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(
value: maxCount > 0 ? counts[i] / maxCount : 0.0,
backgroundColor: const Color(0xFFF0F0F0),
color: const Color(0xFF1A1A1A),
backgroundColor: colors.outlineVariant,
color: colors.primary,
minHeight: 6,
),
),
),
const SizedBox(width: 8),
Text('${counts[i]}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
Text('${counts[i]}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface)),
],
),
)),
@@ -295,6 +301,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
// ─── 月度趋势 ────────────────────────────────────────────────────────
Widget _buildMonthlyTrend(List<Movie> movies, List<Book> books, List<Note> notes) {
final colors = Theme.of(context).colorScheme;
final now = DateTime.now();
final months = List.generate(6, (i) {
final d = DateTime(now.year, now.month - (5 - i), 1);
@@ -323,7 +330,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
final maxVal = allValues.isEmpty ? 1 : allValues.reduce((a, b) => a > b ? a : b);
final safeMax = maxVal == 0 ? 1 : maxVal;
final colors = { '影视': const Color(0xFF4A90D9), '书籍': const Color(0xFF7E57C2), '笔记': const Color(0xFF66BB6A) };
final brandColors = { '影视': const Color(0xFF4A90D9), '书籍': const Color(0xFF7E57C2), '笔记': const Color(0xFF66BB6A) };
return _buildCard(
title: '近6月趋势',
@@ -345,7 +352,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
height: h < 2 && e.value[i] > 0 ? 2 : h,
margin: const EdgeInsets.only(top: 1),
decoration: BoxDecoration(
color: colors[e.key]!.withValues(alpha: 0.7),
color: brandColors[e.key]!.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(2),
),
);
@@ -357,12 +364,12 @@ class _StatisticsPageState extends State<StatisticsPage> {
),
),
const SizedBox(height: 8),
const Divider(height: 1, color: Color(0xFFF0F0F0)),
Divider(height: 1, color: colors.outlineVariant),
const SizedBox(height: 8),
// 月份标签
Row(
children: months.map((m) => Expanded(
child: Text(m, textAlign: TextAlign.center, style: const TextStyle(fontSize: 11, color: Color(0xFFBBBBBB))),
child: Text(m, textAlign: TextAlign.center, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
)).toList(),
),
const SizedBox(height: 12),
@@ -374,9 +381,9 @@ class _StatisticsPageState extends State<StatisticsPage> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 8, height: 8, decoration: BoxDecoration(color: colors[k], borderRadius: BorderRadius.circular(2))),
Container(width: 8, height: 8, decoration: BoxDecoration(color: brandColors[k], borderRadius: BorderRadius.circular(2))),
const SizedBox(width: 4),
Text(k, style: const TextStyle(fontSize: 12, color: Color(0xFF888888))),
Text(k, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5))),
],
),
)).toList(),
@@ -394,10 +401,11 @@ class _StatisticsPageState extends State<StatisticsPage> {
// ─── 通用卡片 ────────────────────────────────────────────────────────
Widget _buildCard({required String title, required Widget child}) {
final colors = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(14),
),
child: Column(
@@ -405,9 +413,9 @@ class _StatisticsPageState extends State<StatisticsPage> {
children: [
Row(
children: [
Container(width: 3, height: 14, decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(2))),
Container(width: 3, height: 14, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(2))),
const SizedBox(width: 8),
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
],
),
const SizedBox(height: 16),

View File

@@ -153,8 +153,9 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: const Text('漫步'),
actions: [
@@ -166,15 +167,15 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(18),
),
child: const Row(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.casino_outlined, size: 14, color: Colors.white),
SizedBox(width: 5),
Text('随机', style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.w500)),
Icon(Icons.casino_outlined, size: 14, color: colors.onPrimary),
const SizedBox(width: 5),
Text('随机', style: TextStyle(fontSize: 12, color: colors.onPrimary, fontWeight: FontWeight.w500)),
],
),
),
@@ -183,8 +184,10 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
],
),
body: _currentItem == null
? const Center(child: Text('还没有任何内容\n去添加一些吧', textAlign: TextAlign.center,
style: TextStyle(fontSize: 15, color: Color(0xFFBBBBBB), height: 1.6)))
? Center(
child: Text('还没有任何内容\n去添加一些吧',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.3), height: 1.6)))
: Consumer<AppProvider>(builder: (context, provider, _) {
final item = _currentItem!;
final hasImage = item.imagePath != null && item.imagePath!.isNotEmpty && File(item.imagePath!).existsSync();
@@ -216,7 +219,7 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
child: Text(
item.title,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A), height: 1.3),
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.3),
),
),
@@ -225,7 +228,7 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(item.subtitle, textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13, color: Color(0xFF999999))),
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))),
),
],
@@ -242,18 +245,18 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: Text(item.detail,
style: const TextStyle(fontSize: 13, color: Color(0xFF777777), height: 1.7)),
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6), height: 1.7)),
),
],
],
// 时间
const SizedBox(height: 12),
Text(_actionText(item), style: const TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))),
Text(_actionText(item), style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.25))),
const SizedBox(height: 40),
],
@@ -302,11 +305,12 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
}
Widget _buildNoteCard(_StrollItem item, bool hasImage) {
final colors = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 360),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.06), blurRadius: 16, offset: const Offset(0, 6)),
@@ -322,12 +326,12 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
Padding(
padding: const EdgeInsets.all(20),
child: Text(item.detail.isEmpty ? '(无内容)' : item.detail,
style: const TextStyle(fontSize: 14, color: Color(0xFF444444), height: 1.8)),
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.73), height: 1.8)),
),
const Divider(height: 1, color: Color(0xFFF0F0F0)),
Divider(height: 1, color: colors.outlineVariant),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Text('${item.detail.length} 字 · Mooknote', style: const TextStyle(fontSize: 11, color: Color(0xFFBBBBBB))),
child: Text('${item.detail.length} 字 · Mooknote', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
),
],
),
@@ -335,8 +339,9 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
}
Widget _buildPlaceholder(_StrollItem item) {
final colors = Theme.of(context).colorScheme;
return Container(
color: const Color(0xFFF8F8F8),
color: colors.surfaceContainerHigh,
child: Center(
child: Icon(item.icon, size: 48, color: item.color.withValues(alpha: 0.2)),
),

View File

@@ -40,71 +40,74 @@ class _BackupPageState extends State<BackupPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(
title: const Text('本地备份'),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.all(24),
children: [
// 自动备份开关 - 紧凑一行
_buildAutoBackupSection(),
padding: const EdgeInsets.all(24),
children: [
// 自动备份开关 - 紧凑一行
_buildAutoBackupSection(colors),
const SizedBox(height: 24),
const SizedBox(height: 24),
// 手动备份
_buildSectionTitle('手动备份'),
const SizedBox(height: 12),
_buildActionCard(
title: '导出数据',
description: '将所有数据导出为 zip 文件,可用于备份或迁移到其他设备',
icon: Icons.upload_outlined,
buttonText: '导出',
isLoading: _isExporting,
onTap: _exportData,
),
const SizedBox(height: 12),
_buildActionCard(
title: '导入数据',
description: '从备份文件导入数据,将覆盖当前所有数据',
icon: Icons.download_outlined,
buttonText: '导入',
isLoading: _isImporting,
onTap: _importData,
isDestructive: true,
),
// 手动备份
_buildSectionTitle(colors, '手动备份'),
const SizedBox(height: 12),
_buildActionCard(
colors: colors,
title: '导出数据',
description: '将所有数据导出为 zip 文件,可用于备份或迁移到其他设备',
icon: Icons.upload_outlined,
buttonText: '导出',
isLoading: _isExporting,
onTap: _exportData,
),
const SizedBox(height: 12),
_buildActionCard(
colors: colors,
title: '导入数据',
description: '从备份文件导入数据,将覆盖当前所有数据',
icon: Icons.download_outlined,
buttonText: '导入',
isLoading: _isImporting,
onTap: _importData,
isDestructive: true,
),
const SizedBox(height: 32),
const SizedBox(height: 32),
// 使用说明
_buildInfoSection(),
],
),
// 使用说明
_buildInfoSection(colors),
],
),
);
}
/// 构建区块标题
Widget _buildSectionTitle(String title) {
Widget _buildSectionTitle(ColorScheme colors, String title) {
return Row(
children: [
Container(
width: 4,
height: 16,
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
],
@@ -113,6 +116,7 @@ class _BackupPageState extends State<BackupPage> {
/// 构建操作卡片
Widget _buildActionCard({
required ColorScheme colors,
required String title,
required String description,
required IconData icon,
@@ -124,7 +128,7 @@ class _BackupPageState extends State<BackupPage> {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: Column(
@@ -136,19 +140,17 @@ class _BackupPageState extends State<BackupPage> {
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isDestructive
? Colors.red.withOpacity(0.3)
: const Color(0xFFE8E8E8),
color: isDestructive ? Colors.red.withOpacity(0.3) : colors.outline,
width: 0.5,
),
),
child: Icon(
icon,
size: 22,
color: isDestructive ? Colors.red : const Color(0xFF666666),
color: isDestructive ? Colors.red : colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(width: 16),
@@ -158,18 +160,18 @@ class _BackupPageState extends State<BackupPage> {
children: [
Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
const SizedBox(height: 4),
Text(
description,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.4,
),
),
@@ -185,25 +187,25 @@ class _BackupPageState extends State<BackupPage> {
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: isLoading ? const Color(0xFFCCCCCC) : const Color(0xFF1A1A1A),
color: isLoading ? colors.onSurface.withValues(alpha: 0.25) : colors.primary,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: isLoading
? const SizedBox(
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(Colors.white),
valueColor: AlwaysStoppedAnimation(colors.onPrimary),
),
)
: Text(
buttonText,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.white,
color: colors.onPrimary,
),
),
),
@@ -215,11 +217,11 @@ class _BackupPageState extends State<BackupPage> {
}
/// 构建信息说明区域
Widget _buildInfoSection() {
Widget _buildInfoSection(ColorScheme colors) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: Column(
@@ -231,42 +233,42 @@ class _BackupPageState extends State<BackupPage> {
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outline, width: 0.5),
),
child: const Icon(
child: Icon(
Icons.info_outline,
size: 18,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(width: 12),
const Text(
Text(
'使用说明',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
],
),
const SizedBox(height: 16),
_buildInfoItem('1', '导出数据会生成一个 .zip 文件,包含所有数据和图片'),
_buildInfoItem(colors, '1', '导出数据会生成一个 .zip 文件,包含所有数据和图片'),
const SizedBox(height: 12),
_buildInfoItem('2', '选择保存路径后,可以通过微信、邮件等方式发送备份文件'),
_buildInfoItem(colors, '2', '选择保存路径后,可以通过微信、邮件等方式发送备份文件'),
const SizedBox(height: 12),
_buildInfoItem('3', '在新设备上选择导入数据,选择备份文件即可恢复'),
_buildInfoItem(colors, '3', '在新设备上选择导入数据,选择备份文件即可恢复'),
const SizedBox(height: 12),
_buildInfoItem('4', '导入数据会完全覆盖当前设备的数据,请谨慎操作'),
_buildInfoItem(colors, '4', '导入数据会完全覆盖当前设备的数据,请谨慎操作'),
],
),
);
}
/// 构建信息项
Widget _buildInfoItem(String number, String text) {
Widget _buildInfoItem(ColorScheme colors, String number, String text) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -274,16 +276,16 @@ class _BackupPageState extends State<BackupPage> {
width: 20,
height: 20,
decoration: BoxDecoration(
color: const Color(0xFFE8E8E8),
color: colors.outline,
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
number,
style: const TextStyle(
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
),
@@ -292,9 +294,9 @@ class _BackupPageState extends State<BackupPage> {
Expanded(
child: Text(
text,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.5,
),
),
@@ -311,85 +313,93 @@ class _BackupPageState extends State<BackupPage> {
}) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: Column(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 24),
),
const SizedBox(height: 16),
Text(title, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
],
),
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 12),
Text(
content,
style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6),
textAlign: TextAlign.center,
),
if (detail != null && detail.isNotEmpty) ...[
const SizedBox(height: 12),
builder: (ctx) {
final colors = Theme.of(ctx).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
width: 48,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
borderRadius: BorderRadius.circular(8),
),
child: Text(
detail,
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
maxLines: 3,
overflow: TextOverflow.ellipsis,
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.check, color: colors.primary, size: 24),
),
const SizedBox(height: 16),
Text(title,
style: TextStyle(
fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
],
],
),
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
style: TextButton.styleFrom(
minimumSize: const Size(120, 40),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('确定', style: TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))),
),
],
),
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 12),
Text(
content,
style: TextStyle(
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6),
textAlign: TextAlign.center,
),
if (detail != null && detail.isNotEmpty) ...[
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
),
child: Text(
detail,
style:
TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
],
),
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
style: TextButton.styleFrom(
minimumSize: const Size(120, 40),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Text('确定', style: TextStyle(fontSize: 14, color: colors.primary)),
),
],
);
},
);
}
/// 导出数据
Future<void> _exportData() async {
setState(() => _isExporting = true);
try {
final result = await BackupService.instance.exportDataWithImages();
if (!mounted) return;
if (result.cancelled) {
ToastUtil.show(context, '已取消导出');
} else if (result.success) {
_showSuccessDialog(
title: '导出成功',
content: '备份文件已保存,包含:\n影视 ${result.movieCount} · 书籍 ${result.bookCount} · 笔记 ${result.noteCount} · 图片 ${result.imageCount}',
content:
'备份文件已保存,包含:\n影视 ${result.movieCount} · 书籍 ${result.bookCount} · 笔记 ${result.noteCount} · 图片 ${result.imageCount}',
detail: result.filePath ?? '',
);
} else {
@@ -411,54 +421,63 @@ class _BackupPageState extends State<BackupPage> {
// 显示确认对话框
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.08),
borderRadius: BorderRadius.circular(10),
builder: (ctx) {
final colors = Theme.of(ctx).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.08),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
),
child: const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
const SizedBox(width: 12),
Text('确认导入',
style: TextStyle(
fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
],
),
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
'导入数据将覆盖当前所有数据,此操作不可恢复。',
style: TextStyle(
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6),
),
),
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Text('取消',
style: TextStyle(
color: colors.onSurface.withValues(alpha: 0.6), fontSize: 14)),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('确认导入',
style: TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.w600)),
),
const SizedBox(width: 12),
const Text('确认导入', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
],
),
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: const Padding(
padding: EdgeInsets.only(top: 16),
child: Text(
'导入数据将覆盖当前所有数据,此操作不可恢复。',
style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6),
),
),
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666), fontSize: 14)),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('确认导入', style: TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.w600)),
),
],
),
);
},
);
if (confirmed != true) return;
@@ -467,7 +486,7 @@ class _BackupPageState extends State<BackupPage> {
try {
final result = await BackupService.instance.importData();
if (!mounted) return;
if (result.cancelled) {
@@ -499,13 +518,13 @@ class _BackupPageState extends State<BackupPage> {
}
/// 构建自动备份区域 - 紧凑一行
Widget _buildAutoBackupSection() {
Widget _buildAutoBackupSection(ColorScheme colors) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outline, width: 0.5),
),
child: Row(
children: [
@@ -513,11 +532,12 @@ class _BackupPageState extends State<BackupPage> {
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outline, width: 0.5),
),
child: const Icon(Icons.schedule, size: 20, color: Color(0xFF666666)),
child: Icon(Icons.schedule,
size: 20, color: colors.onSurface.withValues(alpha: 0.6)),
),
const SizedBox(width: 12),
Expanded(
@@ -525,22 +545,25 @@ class _BackupPageState extends State<BackupPage> {
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'自动本地备份',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface),
),
const SizedBox(height: 2),
Text(
_backupDirPath!,
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
style:
TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
)
: const Text(
: Text(
'自动本地备份',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface),
),
),
Switch(
@@ -555,14 +578,13 @@ class _BackupPageState extends State<BackupPage> {
}
await _loadAutoBackupStatus();
},
activeColor: const Color(0xFF1A1A1A),
activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3),
inactiveThumbColor: Colors.white,
inactiveTrackColor: const Color(0xFFE5E5E5),
activeThumbColor: colors.primary,
activeTrackColor: colors.primary.withValues(alpha: 0.3),
inactiveThumbColor: colors.surface,
inactiveTrackColor: colors.outline,
),
],
),
);
}
}

View File

@@ -8,95 +8,163 @@ class CloudSyncPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF8F8F8),
backgroundColor: colors.surfaceContainerHigh,
appBar: AppBar(title: const Text('云备份')),
body: ListView(
padding: const EdgeInsets.all(20),
children: [
_buildSectionTitle('选择备份方式'),
_buildSectionTitle(colors, '选择备份方式'),
const SizedBox(height: 12),
_buildOption(
colors: colors,
icon: Icons.storage_outlined,
title: 'WebDAV 备份',
subtitle: '通过 WebDAV 协议备份到个人云盘',
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())),
onTap: () =>
Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())),
),
const SizedBox(height: 12),
_buildOption(
colors: colors,
icon: Icons.sync_outlined,
title: '服务端实时同步',
subtitle: '自建服务端,多设备数据实时同步',
enabled: false,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ServerSyncPage())),
onTap: () =>
Navigator.push(context, MaterialPageRoute(builder: (_) => const ServerSyncPage())),
),
const SizedBox(height: 28),
_buildInfo(),
_buildInfo(colors),
],
),
);
}
Widget _buildSectionTitle(String title) {
Widget _buildSectionTitle(ColorScheme colors, String title) {
return Row(children: [
Container(width: 3, height: 14, decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(2))),
Container(
width: 3,
height: 14,
decoration:
BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(2))),
const SizedBox(width: 8),
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
Text(title,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
]);
}
Widget _buildOption({required IconData icon, required String title, required String subtitle, required VoidCallback onTap, bool enabled = true}) {
Widget _buildOption({
required ColorScheme colors,
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
bool enabled = true,
}) {
return GestureDetector(
onTap: enabled ? onTap : null,
child: Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: enabled ? Colors.white : const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(14),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))],
color: enabled ? colors.surface : colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.03),
blurRadius: 6,
offset: const Offset(0, 2)),
],
),
child: Row(children: [
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: enabled ? const Color(0xFF666666) : const Color(0xFFBBBBBB), size: 22)),
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)),
child: Icon(icon,
color: enabled
? colors.onSurface.withValues(alpha: 0.6)
: colors.onSurface.withValues(alpha: 0.3),
size: 22)),
const SizedBox(width: 14),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: enabled ? const Color(0xFF1A1A1A) : const Color(0xFFBBBBBB))),
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(title,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: enabled ? colors.onSurface : colors.onSurface.withValues(alpha: 0.3))),
const SizedBox(height: 3),
Text(subtitle, style: TextStyle(fontSize: 12, color: enabled ? const Color(0xFF999999) : const Color(0xFFCCCCCC))),
Text(subtitle,
style: TextStyle(
fontSize: 12,
color: enabled
? colors.onSurface.withValues(alpha: 0.4)
: colors.onSurface.withValues(alpha: 0.25))),
])),
Icon(Icons.chevron_right, color: enabled ? const Color(0xFFCCCCCC) : const Color(0xFFE5E5E5)),
Icon(Icons.chevron_right,
color: enabled
? colors.onSurface.withValues(alpha: 0.25)
: colors.outline),
]),
),
);
}
Widget _buildInfo() {
Widget _buildInfo(ColorScheme colors) {
return Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))],
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.03),
blurRadius: 6,
offset: const Offset(0, 2)),
],
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Container(width: 36, height: 36, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(8)), child: const Icon(Icons.info_outline, size: 18, color: Color(0xFF666666))),
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(8)),
child: Icon(Icons.info_outline,
size: 18, color: colors.onSurface.withValues(alpha: 0.6))),
const SizedBox(width: 10),
const Text('使用说明', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
Text('使用说明',
style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
]),
const SizedBox(height: 14),
_infoItem('WebDAV 备份:将数据备份到支持 WebDAV 的云盘'),
_infoItem(colors, 'WebDAV 备份:将数据备份到支持 WebDAV 的云盘'),
const SizedBox(height: 8),
_infoItem('服务端实时同步:通过自建服务端实现多设备实时同步'),
_infoItem(colors, '服务端实时同步:通过自建服务端实现多设备实时同步'),
const SizedBox(height: 8),
_infoItem('激活码由服务端管理员在管理后台生成'),
_infoItem(colors, '激活码由服务端管理员在管理后台生成'),
const SizedBox(height: 8),
_infoItem('建议定期备份 + 实时同步配合使用'),
_infoItem(colors, '建议定期备份 + 实时同步配合使用'),
]),
);
}
Widget _infoItem(String text) {
Widget _infoItem(ColorScheme colors, String text) {
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Container(width: 5, height: 5, margin: const EdgeInsets.only(top: 5), decoration: BoxDecoration(color: const Color(0xFFBBBBBB), shape: BoxShape.circle)),
Container(
width: 5,
height: 5,
margin: const EdgeInsets.only(top: 5),
decoration: BoxDecoration(
color: colors.onSurface.withValues(alpha: 0.3), shape: BoxShape.circle)),
const SizedBox(width: 10),
Expanded(child: Text(text, style: const TextStyle(fontSize: 12, color: Color(0xFF888888), height: 1.5))),
Expanded(
child: Text(text,
style: TextStyle(
fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5), height: 1.5))),
]);
}
}

View File

@@ -157,16 +157,24 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
Future<void> _disconnect() async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('断开连接', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
content: const Text('将清除服务器配置和激活信息,确定要断开吗?', style: TextStyle(fontSize: 14, color: Color(0xFF666666))),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定', style: TextStyle(color: Color(0xFFE53935)))),
],
),
builder: (ctx) {
final colors = Theme.of(ctx).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('断开连接', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
content: Text('将清除服务器配置和激活信息,确定要断开吗?',
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6))),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('确定', style: TextStyle(color: Color(0xFFE53935)))),
],
);
},
);
if (confirm != true) return;
@@ -190,63 +198,107 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF8F8F8),
backgroundColor: colors.surfaceContainerHigh,
appBar: AppBar(title: const Text('服务端实时同步')),
body: ListView(padding: const EdgeInsets.all(20), children: [
// 状态卡片
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))],
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))
],
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Container(width: 10, height: 10, decoration: BoxDecoration(
color: _isActivated ? const Color(0xFF66BB6A) : const Color(0xFFDDDDDD), shape: BoxShape.circle)),
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: _isActivated
? const Color(0xFF66BB6A)
: colors.onSurface.withValues(alpha: 0.15),
shape: BoxShape.circle)),
const SizedBox(width: 10),
Text(_isActivated ? '已激活' : '未激活',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600,
color: _isActivated ? const Color(0xFF66BB6A) : const Color(0xFFBBBBBB))),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: _isActivated
? const Color(0xFF66BB6A)
: colors.onSurface.withValues(alpha: 0.3))),
const Spacer(),
if (_isActivated)
GestureDetector(
onTap: _disconnect,
child: Container(padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(6)),
child: const Text('断开', style: TextStyle(fontSize: 12, color: Color(0xFFE57373)))),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(6)),
child: const Text('断开',
style: TextStyle(fontSize: 12, color: Color(0xFFE57373)))),
),
]),
const SizedBox(height: 20),
const Text('服务器地址', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
Text('服务器地址',
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 6),
TextField(controller: _urlController, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
decoration: _inputDeco('例: http://192.168.1.100:5000')),
TextField(
controller: _urlController,
style: TextStyle(fontSize: 14, color: colors.onSurface),
decoration: _inputDeco(colors, '例: http://192.168.1.100:5000'),
),
const SizedBox(height: 14),
const Text('激活码', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
Text('激活码',
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 6),
TextField(controller: _codeController, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
textCapitalization: TextCapitalization.characters, decoration: _inputDeco('例: MK-A1B2C3D4E5F6')),
TextField(
controller: _codeController,
style: TextStyle(fontSize: 14, color: colors.onSurface),
textCapitalization: TextCapitalization.characters,
decoration: _inputDeco(colors, '例: MK-A1B2C3D4E5F6')),
const SizedBox(height: 16),
SizedBox(width: double.infinity,
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isChecking ? null : _checkActivation,
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF1A1A1A), foregroundColor: Colors.white,
disabledBackgroundColor: const Color(0xFFDDDDDD), elevation: 0,
style: ElevatedButton.styleFrom(
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
disabledBackgroundColor: colors.onSurface.withValues(alpha: 0.15),
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
padding: const EdgeInsets.symmetric(vertical: 13)),
child: _isChecking
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: Text(_isActivated ? '重新验证' : '验证激活', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: colors.onPrimary))
: Text(_isActivated ? '重新验证' : '验证激活',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
),
),
if (_expiresText.isNotEmpty) ...[
const SizedBox(height: 12),
Center(child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.access_time, size: 14, color: _prefs.syncIsPermanent ? const Color(0xFF66BB6A) : const Color(0xFFFF9800)),
Center(
child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.access_time,
size: 14,
color: _prefs.syncIsPermanent
? const Color(0xFF66BB6A)
: const Color(0xFFFF9800)),
const SizedBox(width: 6),
Text(_expiresText, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500,
color: _prefs.syncIsPermanent ? const Color(0xFF66BB6A) : const Color(0xFFFF9800))),
Text(_expiresText,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: _prefs.syncIsPermanent
? const Color(0xFF66BB6A)
: const Color(0xFFFF9800))),
])),
],
]),
@@ -255,47 +307,78 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
// 同步开关
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))]),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))
]),
child: Row(children: [
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)),
child: Icon(_syncEnabled ? Icons.sync : Icons.sync_disabled,
color: _syncEnabled ? const Color(0xFF1A1A1A) : const Color(0xFFCCCCCC), size: 22)),
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)),
child: Icon(_syncEnabled ? Icons.sync : Icons.sync_disabled,
color: _syncEnabled
? colors.primary
: colors.onSurface.withValues(alpha: 0.25),
size: 22)),
const SizedBox(width: 14),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('服务端实时同步', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('服务端实时同步',
style: TextStyle(
fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
const SizedBox(height: 2),
Text(_syncEnabled ? '使用服务端数据,多设备实时共享' : '关闭后下载数据到本地使用',
style: const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))),
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))),
])),
Switch(value: _syncEnabled, onChanged: _isActivated ? _toggleSync : null,
activeColor: const Color(0xFF1A1A1A), activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3),
inactiveThumbColor: Colors.white, inactiveTrackColor: const Color(0xFFE5E5E5)),
Switch(
value: _syncEnabled,
onChanged: _isActivated ? _toggleSync : null,
activeThumbColor: colors.primary,
activeTrackColor: colors.primary.withValues(alpha: 0.3),
inactiveThumbColor: colors.surface,
inactiveTrackColor: colors.outline),
]),
),
const SizedBox(height: 24),
// 说明
Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))]),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))
]),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Container(width: 36, height: 36, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(8)),
child: const Icon(Icons.info_outline, size: 18, color: Color(0xFF666666))),
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(8)),
child: Icon(Icons.info_outline,
size: 18, color: colors.onSurface.withValues(alpha: 0.6))),
const SizedBox(width: 10),
const Text('使用说明', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
Text('使用说明',
style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
]),
const SizedBox(height: 14),
_infoItem('1. 在服务端管理后台生成激活码'),
_infoItem(colors, '1. 在服务端管理后台生成激活码'),
const SizedBox(height: 8),
_infoItem('2. 输入服务器地址和激活码完成验证'),
_infoItem(colors, '2. 输入服务器地址和激活码完成验证'),
const SizedBox(height: 8),
_infoItem('3. 验证通过后自动开启实时同步'),
_infoItem(colors, '3. 验证通过后自动开启实时同步'),
const SizedBox(height: 8),
_infoItem('4. 开启时所有数据通过服务端接口操作'),
_infoItem(colors, '4. 开启时所有数据通过服务端接口操作'),
const SizedBox(height: 8),
_infoItem('5. 关闭时从服务端下载数据到本地使用'),
_infoItem(colors, '5. 关闭时从服务端下载数据到本地使用'),
]),
),
const SizedBox(height: 40),
@@ -303,21 +386,33 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
);
}
InputDecoration _inputDeco(String hint) {
InputDecoration _inputDeco(ColorScheme colors, String hint) {
return InputDecoration(
hintText: hint, hintStyle: const TextStyle(fontSize: 13, color: Color(0xFFCCCCCC)),
filled: true, fillColor: const Color(0xFFF8F8F8),
hintText: hint,
hintStyle: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.25)),
filled: true,
fillColor: colors.surfaceContainerHigh,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: colors.primary, width: 1)),
);
}
Widget _infoItem(String text) {
Widget _infoItem(ColorScheme colors, String text) {
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Container(width: 5, height: 5, margin: const EdgeInsets.only(top: 6), decoration: BoxDecoration(color: const Color(0xFFBBBBBB), shape: BoxShape.circle)),
Container(
width: 5,
height: 5,
margin: const EdgeInsets.only(top: 6),
decoration: BoxDecoration(
color: colors.onSurface.withValues(alpha: 0.3), shape: BoxShape.circle)),
const SizedBox(width: 10),
Expanded(child: Text(text, style: const TextStyle(fontSize: 13, color: Color(0xFF888888), height: 1.5))),
Expanded(
child: Text(text,
style: TextStyle(
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5), height: 1.5))),
]);
}
}

View File

@@ -66,22 +66,37 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
final password = _passwordController.text;
final path = _pathController.text.trim();
if (url.isEmpty) { ToastUtil.show(context, '请输入服务器地址'); return; }
if (username.isEmpty) { ToastUtil.show(context, '请输入用户名'); return; }
if (password.isEmpty) { ToastUtil.show(context, '请输入密码'); return; }
if (url.isEmpty) {
ToastUtil.show(context, '请输入服务器地址');
return;
}
if (username.isEmpty) {
ToastUtil.show(context, '请输入用户名');
return;
}
if (password.isEmpty) {
ToastUtil.show(context, '请输入密码');
return;
}
setState(() => _isLoading = true);
try {
final result = await WebDAVService.instance.testConnection(
url: url, username: username, password: password, path: path,
url: url,
username: username,
password: password,
path: path,
);
if (!mounted) return;
if (result['success'] == true) {
await WebDAVService.instance.saveConfig(
url: url, username: username, password: password, path: path,
url: url,
username: username,
password: password,
path: path,
);
setState(() => _isConfigured = true);
ToastUtil.show(context, result['message'] ?? '连接成功,配置已保存');
@@ -104,7 +119,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
if (result.success) {
final details = '上传: ${result.uploadedFiles} 文件, ${result.uploadedImages} 图片\n'
'下载: ${result.downloadedFiles} 文件, ${result.downloadedImages} 图片';
'下载: ${result.downloadedFiles} 文件, ${result.downloadedImages} 图片';
if (result.needReload) {
ToastUtil.show(context, '数据已更新,正在重新加载...');
@@ -129,39 +144,48 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
void _showResultDialog(String title, String content) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: Column(
children: [
Container(
width: 48, height: 48,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(12),
builder: (ctx) {
final colors = Theme.of(ctx).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: Column(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.check, color: colors.primary, size: 24),
),
child: const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 24),
),
const SizedBox(height: 16),
Text(title, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
],
),
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(content, style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6), textAlign: TextAlign.center),
),
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
style: TextButton.styleFrom(minimumSize: const Size(120, 40), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
child: const Text('确定', style: TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))),
const SizedBox(height: 16),
Text(title,
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
],
),
],
),
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(content,
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6),
textAlign: TextAlign.center),
),
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
style: TextButton.styleFrom(
minimumSize: const Size(120, 40),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
child: Text('确定', style: TextStyle(fontSize: 14, color: colors.primary)),
),
],
);
},
);
}
@@ -188,25 +212,40 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
final selected = await showModalBottomSheet<int>(
context: context,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (ctx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
Container(width: 36, height: 4, decoration: BoxDecoration(color: const Color(0xFFDDDDDD), borderRadius: BorderRadius.circular(2))),
const SizedBox(height: 12),
const Text('同步间隔', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
const SizedBox(height: 8),
...intervals.map((i) => ListTile(
title: Text('$i 分钟', style: TextStyle(fontSize: 15, fontWeight: _autoSyncInterval == i ? FontWeight.w600 : FontWeight.w400, color: const Color(0xFF1A1A1A))),
trailing: _autoSyncInterval == i ? const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 20) : null,
onTap: () => Navigator.pop(ctx, i),
)),
const SizedBox(height: 8),
],
),
),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (ctx) {
final colors = Theme.of(ctx).colorScheme;
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
Container(
width: 36,
height: 4,
decoration: BoxDecoration(
color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
const SizedBox(height: 12),
Text('同步间隔',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
const SizedBox(height: 8),
...intervals.map((i) => ListTile(
title: Text('$i 分钟',
style: TextStyle(
fontSize: 15,
fontWeight: _autoSyncInterval == i ? FontWeight.w600 : FontWeight.w400,
color: colors.onSurface)),
trailing: _autoSyncInterval == i
? Icon(Icons.check, color: colors.primary, size: 20)
: null,
onTap: () => Navigator.pop(ctx, i),
)),
const SizedBox(height: 8),
],
),
);
},
);
if (selected != null && selected != _autoSyncInterval) {
@@ -226,41 +265,54 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
Future<void> _clearConfig() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: Row(
children: [
Container(
width: 40, height: 40,
decoration: BoxDecoration(color: Colors.red.withOpacity(0.08), borderRadius: BorderRadius.circular(10)),
child: const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
builder: (ctx) {
final colors = Theme.of(ctx).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.08), borderRadius: BorderRadius.circular(10)),
child: const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
),
const SizedBox(width: 12),
Text('清除配置',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
],
),
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('确定要清除 WebDAV 配置吗?',
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6)),
),
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
child: Text('取消',
style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6), fontSize: 14)),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
child: const Text('清除',
style: TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.w600)),
),
const SizedBox(width: 12),
const Text('清除配置', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
],
),
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: const Padding(
padding: EdgeInsets.only(top: 16),
child: Text('确定要清除 WebDAV 配置吗?', style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6)),
),
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666), fontSize: 14)),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
child: const Text('清除', style: TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.w600)),
),
],
),
);
},
);
if (confirmed == true) {
@@ -281,46 +333,54 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
appBar: AppBar(title: const Text('WebDAV 备份')),
body: _isLoading && !_isConfigured
? const Center(child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A)))
? Center(child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary))
: ListView(
padding: const EdgeInsets.symmetric(horizontal: 24),
children: [
const SizedBox(height: 8),
// 已连接提示
if (_isConfigured) _buildConnectedBanner(),
if (_isConfigured) _buildConnectedBanner(colors),
// 服务器配置
_buildSectionLabel('服务器配置'),
_buildSectionLabel(colors, '服务器配置'),
const SizedBox(height: 16),
_buildInput(
colors: colors,
controller: _urlController,
hint: '服务器地址,如 https://dav.example.com',
icon: Icons.link,
),
const SizedBox(height: 12),
_buildInput(
colors: colors,
controller: _usernameController,
hint: '用户名',
icon: Icons.person_outline,
),
const SizedBox(height: 12),
_buildInput(
colors: colors,
controller: _passwordController,
hint: '密码',
icon: Icons.lock_outline,
obscure: _obscurePassword,
suffix: GestureDetector(
onTap: () => setState(() => _obscurePassword = !_obscurePassword),
child: Icon(_obscurePassword ? Icons.visibility_off : Icons.visibility, size: 20, color: const Color(0xFFBBBBBB)),
child: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
size: 20,
color: colors.onSurface.withValues(alpha: 0.3)),
),
),
const SizedBox(height: 12),
_buildInput(
colors: colors,
controller: _pathController,
hint: '同步路径,如 /mooknote',
icon: Icons.folder_outlined,
@@ -328,14 +388,15 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
const SizedBox(height: 24),
// 测试并保存
_buildBtn('测试并保存', onTap: _isLoading ? null : _saveConfig),
_buildBtn(colors, '测试并保存', onTap: _isLoading ? null : _saveConfig),
const SizedBox(height: 40),
if (_isConfigured) ...[
// 自动同步
_buildSectionLabel('自动同步'),
_buildSectionLabel(colors, '自动同步'),
const SizedBox(height: 12),
_buildSwitchRow(
colors: colors,
icon: Icons.sync,
label: '自动同步',
sub: _isAutoSyncEnabled ? '$_autoSyncInterval 分钟自动同步' : '关闭',
@@ -345,6 +406,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
if (_isAutoSyncEnabled) ...[
const SizedBox(height: 4),
_buildTappableRow(
colors: colors,
label: '同步间隔',
value: '$_autoSyncInterval 分钟',
onTap: _isLoading ? null : _showIntervalPicker,
@@ -354,27 +416,31 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
const SizedBox(height: 32),
// 手动同步
_buildSectionLabel('手动同步'),
_buildSectionLabel(colors, '手动同步'),
const SizedBox(height: 16),
Row(
children: [
Expanded(child: _buildDirectionChip('上传到云端', SyncDirection.upload, Icons.upload)),
Expanded(
child: _buildDirectionChip(
colors, '上传到云端', SyncDirection.upload, Icons.upload)),
const SizedBox(width: 12),
Expanded(child: _buildDirectionChip('下载到本地', SyncDirection.download, Icons.download)),
Expanded(
child: _buildDirectionChip(
colors, '下载到本地', SyncDirection.download, Icons.download)),
],
),
const SizedBox(height: 20),
_buildBtn('立即同步', onTap: _isLoading ? null : _syncData, loading: _isLoading),
_buildBtn(colors, '立即同步', onTap: _isLoading ? null : _syncData, loading: _isLoading),
const SizedBox(height: 32),
// 清除配置
_buildTextBtn('清除配置', onTap: _isLoading ? null : _clearConfig),
_buildTextBtn(colors, '清除配置', onTap: _isLoading ? null : _clearConfig),
const SizedBox(height: 8),
],
const SizedBox(height: 32),
_buildTips(),
_buildTips(colors),
const SizedBox(height: 60),
],
),
@@ -383,40 +449,48 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
// ── widgets ─────────────────────────────────────────
Widget _buildConnectedBanner() {
Widget _buildConnectedBanner(ColorScheme colors) {
return Container(
margin: const EdgeInsets.only(bottom: 24),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
width: 6, height: 6,
width: 6,
height: 6,
decoration: const BoxDecoration(color: Color(0xFF4CAF50), shape: BoxShape.circle),
),
const SizedBox(width: 10),
Expanded(
child: Text(
_urlController.text,
style: const TextStyle(fontSize: 13, color: Color(0xFF666666)),
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Text('已连接', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
Text('已连接',
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
],
),
);
}
Widget _buildSectionLabel(String text) {
return Text(text, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF999999), letterSpacing: 0.5));
Widget _buildSectionLabel(ColorScheme colors, String text) {
return Text(text,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: colors.onSurface.withValues(alpha: 0.4),
letterSpacing: 0.5));
}
Widget _buildInput({
required ColorScheme colors,
required TextEditingController controller,
required String hint,
required IconData icon,
@@ -426,35 +500,37 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
return TextField(
controller: controller,
obscureText: obscure,
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
style: TextStyle(fontSize: 15, color: colors.onSurface),
decoration: InputDecoration(
hintText: hint,
hintStyle: const TextStyle(fontSize: 15, color: Color(0xFFBBBBBB)),
hintStyle: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.3)),
prefixIcon: Padding(
padding: const EdgeInsets.only(left: 4, right: 8),
child: Icon(icon, size: 20, color: const Color(0xFFBBBBBB)),
child: Icon(icon, size: 20, color: colors.onSurface.withValues(alpha: 0.3)),
),
prefixIconConstraints: const BoxConstraints(minWidth: 44),
suffixIcon: suffix != null ? Padding(
padding: const EdgeInsets.only(right: 8),
child: suffix,
) : null,
suffixIcon: suffix != null
? Padding(
padding: const EdgeInsets.only(right: 8),
child: suffix,
)
: null,
filled: true,
fillColor: const Color(0xFFF8F8F8),
fillColor: colors.surfaceContainerHigh,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1),
borderSide: BorderSide(color: colors.primary, width: 1),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
),
);
}
Widget _buildBtn(String text, {VoidCallback? onTap, bool loading = false}) {
Widget _buildBtn(ColorScheme colors, String text, {VoidCallback? onTap, bool loading = false}) {
final disabled = onTap == null;
return GestureDetector(
onTap: onTap,
@@ -462,30 +538,44 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 15),
decoration: BoxDecoration(
color: disabled ? const Color(0xFFDDDDDD) : const Color(0xFF1A1A1A),
color: disabled ? colors.onSurface.withValues(alpha: 0.15) : colors.primary,
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: loading
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white)))
: Text(text, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white)),
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2, valueColor: AlwaysStoppedAnimation(colors.onPrimary)))
: Text(text,
style: TextStyle(
fontSize: 15, fontWeight: FontWeight.w600, color: colors.onPrimary)),
),
),
);
}
Widget _buildTextBtn(String text, {VoidCallback? onTap}) {
Widget _buildTextBtn(ColorScheme colors, String text, {VoidCallback? onTap}) {
return GestureDetector(
onTap: onTap,
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(text, style: TextStyle(fontSize: 14, color: onTap == null ? const Color(0xFFCCCCCC) : const Color(0xFF999999)))),
child: Text(
text,
style: TextStyle(
fontSize: 14,
color: onTap == null
? colors.onSurface.withValues(alpha: 0.25)
: colors.onSurface.withValues(alpha: 0.4))),
),
),
);
}
Widget _buildSwitchRow({
required ColorScheme colors,
required IconData icon,
required String label,
required String sub,
@@ -495,26 +585,30 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Icon(icon, size: 20, color: value ? const Color(0xFF1A1A1A) : const Color(0xFFBBBBBB)),
Icon(icon,
size: 20,
color: value ? colors.primary : colors.onSurface.withValues(alpha: 0.3)),
const SizedBox(width: 12),
Expanded(
child: Text(
value ? sub : label,
style: TextStyle(fontSize: 14, color: value ? const Color(0xFF666666) : const Color(0xFF1A1A1A)),
style: TextStyle(
fontSize: 14,
color: value ? colors.onSurface.withValues(alpha: 0.6) : colors.onSurface),
),
),
Switch(
value: value,
onChanged: onChanged,
activeColor: const Color(0xFF1A1A1A),
activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3),
inactiveThumbColor: Colors.white,
inactiveTrackColor: const Color(0xFFDDDDDD),
activeThumbColor: colors.primary,
activeTrackColor: colors.primary.withValues(alpha: 0.3),
inactiveThumbColor: colors.surface,
inactiveTrackColor: colors.onSurface.withValues(alpha: 0.15),
),
],
),
@@ -522,6 +616,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
}
Widget _buildTappableRow({
required ColorScheme colors,
required String label,
required String value,
VoidCallback? onTap,
@@ -531,77 +626,99 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
const SizedBox(width: 32),
Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF999999))),
Text(label,
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4))),
const Spacer(),
Text(value, style: const TextStyle(fontSize: 14, color: Color(0xFF666666))),
Text(value,
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6))),
const SizedBox(width: 4),
const Icon(Icons.chevron_right, size: 16, color: Color(0xFFCCCCCC)),
Icon(Icons.chevron_right,
size: 16, color: colors.onSurface.withValues(alpha: 0.25)),
],
),
),
);
}
Widget _buildDirectionChip(String label, SyncDirection dir, IconData icon) {
Widget _buildDirectionChip(ColorScheme colors, String label, SyncDirection dir, IconData icon) {
final selected = _syncDirection == dir;
return GestureDetector(
onTap: () => setState(() => _syncDirection = dir),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFF8F8F8),
color: selected ? colors.primary : colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 16, color: selected ? Colors.white : const Color(0xFF999999)),
Icon(icon,
size: 16,
color: selected
? colors.onPrimary
: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 6),
Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: selected ? Colors.white : const Color(0xFF666666))),
Text(label,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: selected
? colors.onPrimary
: colors.onSurface.withValues(alpha: 0.6))),
],
),
),
);
}
Widget _buildTips() {
Widget _buildTips(ColorScheme colors) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('支持的服务', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF999999), letterSpacing: 0.5)),
Text('支持的服务',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: colors.onSurface.withValues(alpha: 0.4),
letterSpacing: 0.5)),
const SizedBox(height: 10),
_tip('坚果云、Nextcloud、AList 等 WebDAV 服务'),
_tip('服务器地址需包含 https://'),
_tip('首次同步可能需要较长时间'),
_tip(colors, '坚果云、Nextcloud、AList 等 WebDAV 服务'),
_tip(colors, '服务器地址需包含 https://'),
_tip(colors, '首次同步可能需要较长时间'),
],
),
);
}
Widget _tip(String text) {
Widget _tip(ColorScheme colors, String text) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(top: 8),
child: Icon(Icons.circle, size: 4, color: Color(0xFFCCCCCC)),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Icon(Icons.circle,
size: 4, color: colors.onSurface.withValues(alpha: 0.25)),
),
const SizedBox(width: 8),
Expanded(child: Text(text, style: const TextStyle(fontSize: 13, color: Color(0xFF888888), height: 1.5))),
Expanded(
child: Text(text,
style: TextStyle(
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5), height: 1.5))),
],
),
);

View File

@@ -48,7 +48,6 @@ class _TagManagementPageState extends State<TagManagementPage> {
String get _currentType => _tabTypes[_currentIndex];
/// 计算标签使用次数
Map<String, int> _getTagUsageCounts(String type) {
final provider = context.read<AppProvider>();
final counts = <String, int>{};
@@ -81,16 +80,17 @@ class _TagManagementPageState extends State<TagManagementPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF8F8F8),
backgroundColor: colors.surfaceContainerHigh,
appBar: AppBar(
title: const Text('标签管理'),
actions: [
_isSyncing
? const Padding(
padding: EdgeInsets.all(16),
? Padding(
padding: const EdgeInsets.all(16),
child: SizedBox(width: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A))),
child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)),
)
: IconButton(
icon: const Icon(Icons.sync, size: 20),
@@ -112,7 +112,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 12, offset: const Offset(0, 4)),
@@ -121,10 +121,10 @@ class _TagManagementPageState extends State<TagManagementPage> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.add, size: 18, color: Colors.white),
Icon(Icons.add, size: 18, color: colors.onPrimary),
const SizedBox(width: 6),
Text('添加${_typeLabels[_currentIndex]}',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)),
],
),
),
@@ -132,14 +132,13 @@ class _TagManagementPageState extends State<TagManagementPage> {
);
}
// ─── Tab 选择器 ──────────────────────────────────────────────────────
Widget _buildTabSelector() {
final colors = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: const Color(0xFFEBEBEB),
color: colors.outlineVariant,
borderRadius: BorderRadius.circular(24),
),
child: LayoutBuilder(
@@ -158,7 +157,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
padding: const EdgeInsets.all(3),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(22),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, 2)),
@@ -183,7 +182,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
alignment: Alignment.center,
child: Text(_typeLabels[i],
style: TextStyle(fontSize: 13, fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
color: selected ? const Color(0xFF1A1A1A) : const Color(0xFF999999))),
color: selected ? colors.onSurface : colors.onSurface.withValues(alpha: 0.4))),
),
),
);
@@ -197,9 +196,8 @@ class _TagManagementPageState extends State<TagManagementPage> {
);
}
// ─── 标签列表 ────────────────────────────────────────────────────────
Widget _buildTagList(String type) {
final colors = Theme.of(context).colorScheme;
final tags = _tagCache[type] ?? [];
final usageCounts = _getTagUsageCounts(type);
@@ -207,7 +205,6 @@ class _TagManagementPageState extends State<TagManagementPage> {
return _buildEmptyState(type);
}
// 按使用次数降序排序
final sorted = List<Map<String, dynamic>>.from(tags)
..sort((a, b) {
final ca = usageCounts[a['name']] ?? 0;
@@ -220,18 +217,16 @@ class _TagManagementPageState extends State<TagManagementPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 统计行
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Text('${tags.length} 个标签', style: const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))),
Text('${tags.length} 个标签', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))),
const Spacer(),
Text('已使用 ${usageCounts.length}', style: const TextStyle(fontSize: 12, color: Color(0xFFD0D0D0))),
Text('已使用 ${usageCounts.length}', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.2))),
],
),
),
// 标签列表
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 80),
@@ -248,50 +243,50 @@ class _TagManagementPageState extends State<TagManagementPage> {
}
Widget _buildTagChip(Map<String, dynamic> tag, Map<String, int> usageCounts) {
final colors = Theme.of(context).colorScheme;
final name = tag['name'] as String;
final count = usageCounts[name] ?? 0;
return Container(
padding: const EdgeInsets.only(left: 14, right: 6, top: 8, bottom: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: const Offset(0, 2)),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 标签名区域 — 点击改名
GestureDetector(
onTap: () => _showRenameDialog(tag),
child: Text(name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
),
if (count > 0) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(color: const Color(0xFFF0F0F0), borderRadius: BorderRadius.circular(8)),
child: Text('$count', style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: Color(0xFF999999))),
return GestureDetector(
onLongPress: () => _showRenameDialog(tag),
child: Container(
padding: const EdgeInsets.only(left: 14, right: 6, top: 8, bottom: 8),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: const Offset(0, 2)),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(name, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface)),
if (count > 0) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(color: colors.outlineVariant, borderRadius: BorderRadius.circular(8)),
child: Text('$count', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.4))),
),
],
const SizedBox(width: 4),
GestureDetector(
onTap: () => _showDeleteDialog(tag),
child: Container(
width: 22, height: 22,
decoration: BoxDecoration(color: colors.outlineVariant, borderRadius: BorderRadius.circular(11)),
child: Icon(Icons.close, size: 12, color: colors.onSurface.withValues(alpha: 0.4)),
),
),
],
const SizedBox(width: 4),
// X 按钮 — 点击删除
GestureDetector(
onTap: () => _showDeleteDialog(tag),
child: Container(
width: 22, height: 22,
decoration: BoxDecoration(color: const Color(0xFFF0F0F0), borderRadius: BorderRadius.circular(11)),
child: const Icon(Icons.close, size: 12, color: Color(0xFF999999)),
),
),
],
),
),
);
}
Widget _buildEmptyState(String type) {
final colors = Theme.of(context).colorScheme;
final idx = _tabTypes.indexOf(type);
final icon = _typeIcons[idx];
final label = _typeLabels[idx];
@@ -303,57 +298,56 @@ class _TagManagementPageState extends State<TagManagementPage> {
children: [
Container(
width: 64, height: 64,
decoration: BoxDecoration(color: const Color(0xFFECECEC), borderRadius: BorderRadius.circular(18)),
child: Icon(icon, size: 28, color: const Color(0xFFCCCCCC)),
decoration: BoxDecoration(color: colors.outlineVariant, borderRadius: BorderRadius.circular(18)),
child: Icon(icon, size: 28, color: colors.onSurface.withValues(alpha: 0.25)),
),
const SizedBox(height: 16),
Text('暂无$label',
style: const TextStyle(fontSize: 14, color: Color(0xFFBBBBBB), fontWeight: FontWeight.w500)),
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.3), fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
Text(hints[idx], style: const TextStyle(fontSize: 12, color: Color(0xFFD5D5D5))),
Text(hints[idx], style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.15))),
],
),
);
}
// ─── 添加标签 ────────────────────────────────────────────────────────
void _showAddDialog() {
final colors = Theme.of(context).colorScheme;
final controller = TextEditingController();
final type = _currentType;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Text('添加${_typeLabels[_currentIndex]}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
content: TextField(
controller: controller, autofocus: true,
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
style: TextStyle(fontSize: 15, color: colors.onSurface),
decoration: InputDecoration(
hintText: '输入标签名称',
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)),
filled: true, fillColor: const Color(0xFFFAFAFA),
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
filled: true, fillColor: colors.surfaceContainerHigh,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1)),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: colors.primary, width: 1)),
),
onSubmitted: (value) => _doAddTag(ctx, controller.text.trim(), type),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))),
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
Container(
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(20)),
decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _doAddTag(ctx, controller.text.trim(), type),
borderRadius: BorderRadius.circular(20),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text('添加', style: TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text('添加', style: TextStyle(fontSize: 14, color: colors.onPrimary, fontWeight: FontWeight.w500)),
),
),
),
@@ -378,9 +372,8 @@ class _TagManagementPageState extends State<TagManagementPage> {
await _loadTags(type);
}
// ─── 重命名 ──────────────────────────────────────────────────────────
void _showRenameDialog(Map<String, dynamic> tag) {
final colors = Theme.of(context).colorScheme;
final controller = TextEditingController(text: tag['name'] as String);
final tagId = tag['id'] as String;
final type = tag['type'] as String;
@@ -389,34 +382,34 @@ class _TagManagementPageState extends State<TagManagementPage> {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('重命名标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
title: Text('重命名标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
content: TextField(
controller: controller, autofocus: true,
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
style: TextStyle(fontSize: 15, color: colors.onSurface),
decoration: InputDecoration(
hintText: '输入新名称',
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)),
filled: true, fillColor: const Color(0xFFFAFAFA),
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
filled: true, fillColor: colors.surfaceContainerHigh,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1)),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: colors.primary, width: 1)),
),
onSubmitted: (value) => _doRenameTag(ctx, tagId, value.trim(), type, oldName),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))),
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
Container(
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(20)),
decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _doRenameTag(ctx, tagId, controller.text.trim(), type, oldName),
borderRadius: BorderRadius.circular(20),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text('确定', style: TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text('确定', style: TextStyle(fontSize: 14, color: colors.onPrimary, fontWeight: FontWeight.w500)),
),
),
),
@@ -439,9 +432,8 @@ class _TagManagementPageState extends State<TagManagementPage> {
if (success) await _loadTags(type);
}
// ─── 删除(长按触发)──────────────────────────────────────────────────
void _showDeleteDialog(Map<String, dynamic> tag) {
final colors = Theme.of(context).colorScheme;
final tagId = tag['id'] as String;
final type = tag['type'] as String;
final name = tag['name'] as String;
@@ -457,17 +449,17 @@ class _TagManagementPageState extends State<TagManagementPage> {
context: context,
builder: (ctx) => StatefulBuilder(
builder: (ctx, setDialogState) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(12)),
child: Text(name, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))),
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(12)),
child: Text(name, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6))),
),
const SizedBox(width: 10),
const Text('删除标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
Text('删除标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
],
),
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
@@ -481,7 +473,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
const Text('删除后对已有条目的影响:', style: TextStyle(fontSize: 13, color: Color(0xFF999999))),
Text('删除后对已有条目的影响:', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 12),
_buildDeleteOption(value: 'remove', groupValue: selectedAction, onChanged: (v) => setDialogState(() => selectedAction = v), title: '从所有条目中移除该标签', subtitle: '标签将从影视/书籍/笔记中清除'),
const SizedBox(height: 4),
@@ -504,19 +496,19 @@ class _TagManagementPageState extends State<TagManagementPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5),
color: isSelected ? colors.primary : colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: isSelected ? colors.primary : colors.outlineVariant, width: 0.5),
),
child: Text(t, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: isSelected ? Colors.white : const Color(0xFF555555))),
child: Text(t, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.7))),
),
);
}).toList(),
)
: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(12)),
child: const Text('无其他标签可替换', style: TextStyle(fontSize: 13, color: Color(0xFFAAAAAA))),
decoration: BoxDecoration(color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(12)),
child: Text('无其他标签可替换', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.35))),
),
),
],
@@ -526,7 +518,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
),
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))),
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
Container(
decoration: BoxDecoration(color: const Color(0xFFE53935), borderRadius: BorderRadius.circular(20)),
child: Material(
@@ -573,15 +565,16 @@ class _TagManagementPageState extends State<TagManagementPage> {
required String title,
String? subtitle,
}) {
final colors = Theme.of(context).colorScheme;
final selected = value == groupValue;
return GestureDetector(
onTap: () => onChanged(value),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: selected ? const Color(0xFFFAFAFA) : Colors.white,
color: selected ? colors.surfaceContainerHigh : colors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFEEEEEE), width: selected ? 1 : 0.5),
border: Border.all(color: selected ? colors.primary : colors.outlineVariant, width: selected ? 1 : 0.5),
),
child: Row(
children: [
@@ -589,14 +582,14 @@ class _TagManagementPageState extends State<TagManagementPage> {
width: 18, height: 18,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFCCCCCC), width: selected ? 5 : 1.5),
border: Border.all(color: selected ? colors.primary : colors.onSurface.withValues(alpha: 0.25), width: selected ? 5 : 1.5),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
Text(title, style: TextStyle(fontSize: 14, fontWeight: selected ? FontWeight.w500 : FontWeight.normal, color: selected ? const Color(0xFF1A1A1A) : const Color(0xFF666666))),
if (subtitle != null) Padding(padding: const EdgeInsets.only(top: 2), child: Text(subtitle, style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)))),
Text(title, style: TextStyle(fontSize: 14, fontWeight: selected ? FontWeight.w500 : FontWeight.normal, color: selected ? colors.onSurface : colors.onSurface.withValues(alpha: 0.6))),
if (subtitle != null) Padding(padding: const EdgeInsets.only(top: 2), child: Text(subtitle, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35)))),
]),
),
],

View File

@@ -32,13 +32,16 @@ class AppProvider extends ChangeNotifier {
// 当前主界面选中的标签 (0: 观影1: 阅读2: 笔记)
int _mainTabIndex = 0;
// 当前底部导航选中的索引 (0: 主页1: 新增2: 我的)
int _bottomNavIndex = 0;
// 底部导航栏是否可见
bool _bottomNavVisible = true;
// 主题模式
ThemeMode _themeMode = ThemeMode.system;
/// 是否使用远程服务端(同步开关 + 已激活)
bool get _useRemote {
final prefs = UserPrefs();
@@ -134,6 +137,7 @@ class AppProvider extends ChangeNotifier {
int get bookStatusIndex => _bookStatusIndex;
bool get drawerOpen => _drawerOpen;
bool get bottomNavVisible => _bottomNavVisible;
ThemeMode get themeMode => _themeMode;
List<Movie> get movies => _movies;
List<Book> get books => _books;
List<Note> get notes => _notes;
@@ -167,6 +171,26 @@ class AppProvider extends ChangeNotifier {
}
}
void setThemeMode(ThemeMode mode) {
if (_themeMode != mode) {
_themeMode = mode;
notifyListeners();
}
}
void loadThemeMode() {
final prefs = UserPrefs();
switch (prefs.themeMode) {
case 1:
_themeMode = ThemeMode.light;
case 2:
_themeMode = ThemeMode.dark;
default:
_themeMode = ThemeMode.system;
}
notifyListeners();
}
void setMovieStatusIndex(int index) {
_movieStatusIndex = index;
notifyListeners();

View File

@@ -51,6 +51,10 @@ class AppTheme {
onSurface: _black,
error: error,
onError: _white,
surfaceContainerHighest: _offWhite,
surfaceContainerHigh: Color(0xFFFAFAFA),
outline: _lighterGray,
outlineVariant: Color(0xFFF0F0F0),
),
// AppBar - 极简无边框
@@ -262,6 +266,10 @@ class AppTheme {
onSurface: _white,
error: Color(0xFFEF4444),
onError: _black,
surfaceContainerHighest: _darkGray,
surfaceContainerHigh: Color(0xFF2A2A2A),
outline: _gray,
outlineVariant: _darkGray,
),
appBarTheme: const AppBarTheme(
@@ -286,6 +294,12 @@ class AppTheme {
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
margin: EdgeInsets.zero,
),
listTileTheme: const ListTileThemeData(
contentPadding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
minLeadingWidth: 0,
dense: true,
),
dividerTheme: DividerThemeData(
color: _darkGray,
@@ -304,6 +318,9 @@ class AppTheme {
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: _white, width: 1),
),
errorBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFFEF4444), width: 0.5),
),
contentPadding: EdgeInsets.symmetric(vertical: 12),
hintStyle: TextStyle(
fontFamily: _fontFamily,
@@ -311,6 +328,12 @@ class AppTheme {
fontWeight: _regular,
color: _gray,
),
labelStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 13,
fontWeight: _medium,
color: _lightGray,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
@@ -327,6 +350,11 @@ class AppTheme {
style: TextButton.styleFrom(
foregroundColor: _white,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
textStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 14,
fontWeight: _medium,
),
),
),
@@ -335,6 +363,84 @@ class AppTheme {
selectedItemColor: _white,
unselectedItemColor: _gray,
elevation: 0,
type: BottomNavigationBarType.fixed,
selectedLabelStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 11,
fontWeight: _medium,
),
unselectedLabelStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 11,
fontWeight: _regular,
),
),
textTheme: const TextTheme(
headlineLarge: TextStyle(
fontFamily: _fontFamily,
fontSize: 32,
fontWeight: _semibold,
color: _white,
letterSpacing: -0.5,
height: 1.2,
),
headlineMedium: TextStyle(
fontFamily: _fontFamily,
fontSize: 24,
fontWeight: _semibold,
color: _white,
letterSpacing: -0.3,
height: 1.3,
),
headlineSmall: TextStyle(
fontFamily: _fontFamily,
fontSize: 20,
fontWeight: _semibold,
color: _white,
letterSpacing: -0.2,
height: 1.4,
),
bodyLarge: TextStyle(
fontFamily: _fontFamily,
fontSize: 16,
fontWeight: _regular,
color: _offWhite,
height: 1.6,
),
bodyMedium: TextStyle(
fontFamily: _fontFamily,
fontSize: 15,
fontWeight: _regular,
color: _offWhite,
height: 1.5,
),
bodySmall: TextStyle(
fontFamily: _fontFamily,
fontSize: 13,
fontWeight: _regular,
color: _lightGray,
height: 1.5,
),
labelLarge: TextStyle(
fontFamily: _fontFamily,
fontSize: 14,
fontWeight: _medium,
color: _white,
),
labelMedium: TextStyle(
fontFamily: _fontFamily,
fontSize: 12,
fontWeight: _medium,
color: _lightGray,
),
labelSmall: TextStyle(
fontFamily: _fontFamily,
fontSize: 11,
fontWeight: _medium,
color: _gray,
letterSpacing: 0.3,
),
),
);
}

View File

@@ -17,8 +17,8 @@ class UsageStatsService with WidgetsBindingObserver {
final UserPrefs _prefs = UserPrefs();
/// 统计服务器地址,发布前替换为实际地址,置空则禁用
static String serverUrl = 'http://api.mooknote.iletter.top/';
// static String serverUrl = 'http://api.mooknote.iletter.top/';
static String serverUrl = 'http://192.168.31.48:27050/';
Timer? _heartbeatTimer;
bool _started = false;

View File

@@ -11,6 +11,11 @@ class UserPrefs {
/// 初始化
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
// 迁移旧版 isDarkMode 布尔值到新版 themeMode 三态值
if (_prefs!.containsKey('isDarkMode') && !_prefs!.containsKey('themeMode')) {
final oldValue = _prefs!.getBool('isDarkMode') ?? false;
await _prefs!.setInt('themeMode', oldValue ? 2 : 0);
}
}
/// 获取实例
@@ -38,9 +43,9 @@ class UserPrefs {
// ========== 应用设置 ==========
/// 是否暗黑模式
bool get isDarkMode => prefs.getBool('isDarkMode') ?? false;
Future<bool> setDarkMode(bool value) => prefs.setBool('isDarkMode', value);
/// 主题模式: 0=跟随系统, 1=浅色, 2=深色
int get themeMode => prefs.getInt('themeMode') ?? 0;
Future<bool> setThemeMode(int value) => prefs.setInt('themeMode', value);
/// 是否首次启动
bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true;

View File

@@ -55,6 +55,7 @@ class _AnimatedStarRatingState extends State<AnimatedStarRating>
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final starValue = widget.rating / 2;
return Row(
mainAxisSize: MainAxisSize.min,
@@ -84,7 +85,7 @@ class _AnimatedStarRatingState extends State<AnimatedStarRating>
style: TextStyle(
fontSize: widget.starSize,
fontWeight: FontWeight.w600,
color: const Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
);
},

View File

@@ -15,10 +15,11 @@ class AppRefreshIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator(
onRefresh: onRefresh,
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
color: colors.primary,
backgroundColor: colors.surface,
strokeWidth: 2.5,
displacement: 60,
semanticsLabel: semanticsLabel,

View File

@@ -15,6 +15,7 @@ class BookListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: () {
Navigator.pushNamed(context, '/book-detail', arguments: book);
@@ -23,28 +24,21 @@ class BookListItem extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 封面
Expanded(
child: _buildCover(),
child: _buildCover(colors),
),
const SizedBox(height: 8),
// 书名
Text(
book.title,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
// 评分
if (book.rating != null)
AnimatedStarRating(rating: book.rating!, starSize: 12, showNumber: true)
else
@@ -53,45 +47,45 @@ class BookListItem extends StatelessWidget {
),
);
}
/// 构建封面
Widget _buildCover() {
Widget _buildCover(ColorScheme colors) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5),
border: Border.all(color: colors.outline, width: 0.5),
),
clipBehavior: Clip.antiAlias,
child: FadeInLocalImage(
path: book.coverPath,
fit: BoxFit.cover,
placeholder: const Center(child: Icon(Icons.menu_book_outlined, size: 32, color: Color(0xFFCCCCCC))),
errorWidget: const Center(child: Icon(Icons.menu_book_outlined, size: 32, color: Color(0xFFCCCCCC))),
placeholder: Center(child: Icon(Icons.menu_book_outlined, size: 32, color: colors.onSurface.withValues(alpha: 0.25))),
errorWidget: Center(child: Icon(Icons.menu_book_outlined, size: 32, color: colors.onSurface.withValues(alpha: 0.25))),
),
);
}
/// 显示删除确认对话框
void _showDeleteDialog(BuildContext context) {
final colors = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text(
title: Text(
'确认删除',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
content: Text(
'确定要删除《${book.title}》吗?删除后可在回收站恢复。',
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.5,
),
),
@@ -99,7 +93,7 @@ class BookListItem extends StatelessWidget {
TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF666666),
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('取消'),
@@ -111,8 +105,8 @@ class BookListItem extends StatelessWidget {
ToastUtil.show(context, '已删除');
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
backgroundColor: colors.error,
foregroundColor: colors.onError,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),

View File

@@ -8,20 +8,21 @@ class BookStatusBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Consumer<AppProvider>(
builder: (context, provider, child) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: const BoxDecoration(
color: Colors.white,
decoration: BoxDecoration(
color: colors.surface,
border: Border(
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
bottom: BorderSide(color: colors.outline, width: 0.5),
),
),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(24),
),
child: LayoutBuilder(
@@ -42,7 +43,7 @@ class BookStatusBar extends StatelessWidget {
padding: const EdgeInsets.all(3),
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
@@ -58,18 +59,21 @@ class BookStatusBar extends StatelessWidget {
Row(
children: [
_buildTab(
colors: colors,
label: '已读',
icon: Icons.check_circle_outline,
isSelected: provider.bookStatusIndex == 0,
onTap: () => provider.setBookStatusIndex(0),
),
_buildTab(
colors: colors,
label: '在读',
icon: Icons.menu_book_outlined,
isSelected: provider.bookStatusIndex == 1,
onTap: () => provider.setBookStatusIndex(1),
),
_buildTab(
colors: colors,
label: '想读',
icon: Icons.bookmark_outlined,
isSelected: provider.bookStatusIndex == 2,
@@ -89,6 +93,7 @@ class BookStatusBar extends StatelessWidget {
}
Widget _buildTab({
required ColorScheme colors,
required String label,
required IconData icon,
required bool isSelected,
@@ -108,7 +113,7 @@ class BookStatusBar extends StatelessWidget {
Icon(
icon,
size: 16,
color: isSelected ? Colors.white : const Color(0xFF666666),
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
),
const SizedBox(width: 6),
Text(
@@ -116,7 +121,7 @@ class BookStatusBar extends StatelessWidget {
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? Colors.white : const Color(0xFF666666),
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
),
),
],

View File

@@ -8,34 +8,33 @@ class CustomBottomNavBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 获取底部安全区域高度
final bottomPadding = MediaQuery.of(context).padding.bottom;
final colors = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Consumer<AppProvider>(
builder: (context, provider, child) {
return Container(
// 高度:导航栏本身高度 + 底部安全距离 + 上下边距
height: 64 + bottomPadding + 16,
color: Colors.transparent,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Dock栏主体 - 悬浮效果
Container(
height: 56,
margin: const EdgeInsets.symmetric(horizontal: 40),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
color: Colors.black.withOpacity(isDark ? 0.3 : 0.08),
blurRadius: 20,
offset: const Offset(0, 4),
spreadRadius: 0,
),
BoxShadow(
color: Colors.black.withOpacity(0.04),
color: Colors.black.withOpacity(isDark ? 0.15 : 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
spreadRadius: -2,
@@ -45,19 +44,16 @@ class CustomBottomNavBar extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 主页按钮
_buildNavItem(
colors: colors,
icon: Icons.home_outlined,
activeIcon: Icons.home,
isActive: provider.bottomNavIndex == 0,
onTap: () => provider.setBottomNavIndex(0),
),
// 中间新增按钮
_buildAddButton(context, provider),
// 我的按钮
_buildNavItem(
colors: colors,
icon: Icons.person_outline,
activeIcon: Icons.person,
isActive: provider.bottomNavIndex == 2,
@@ -66,7 +62,6 @@ class CustomBottomNavBar extends StatelessWidget {
],
),
),
// 底部安全距离占位
SizedBox(height: bottomPadding + 8),
],
),
@@ -74,9 +69,9 @@ class CustomBottomNavBar extends StatelessWidget {
},
);
}
/// 构建导航项
Widget _buildNavItem({
required ColorScheme colors,
required IconData icon,
required IconData activeIcon,
required bool isActive,
@@ -92,16 +87,16 @@ class CustomBottomNavBar extends StatelessWidget {
child: Center(
child: Icon(
isActive ? activeIcon : icon,
color: isActive ? const Color(0xFF1A1A1A) : const Color(0xFF999999),
color: isActive ? colors.primary : colors.onSurface.withValues(alpha: 0.4),
size: 26,
),
),
),
);
}
/// 构建中间新增按钮
Widget _buildAddButton(BuildContext context, AppProvider provider) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: () => _showAddDialog(context, provider),
onLongPress: () => _showQuickAddDialog(context, provider),
@@ -109,25 +104,23 @@ class CustomBottomNavBar extends StatelessWidget {
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
child: Icon(
Icons.add,
color: Colors.white,
color: colors.onPrimary,
size: 24,
),
),
);
}
/// 长按快速添加 - 根据当前界面直接跳转到对应添加界面
void _showQuickAddDialog(BuildContext context, AppProvider provider) {
// 根据当前主标签页决定跳转到哪个添加界面
final currentTab = provider.mainTabIndex;
switch (currentTab) {
case 0: // 观影标签页
case 0:
final statusMap = {
0: 'watched',
1: 'watching',
@@ -140,7 +133,7 @@ class CustomBottomNavBar extends StatelessWidget {
arguments: {'initialStatus': currentStatus},
);
break;
case 1: // 阅读标签页
case 1:
final statusMap = {
0: 'read',
1: 'reading',
@@ -153,7 +146,7 @@ class CustomBottomNavBar extends StatelessWidget {
arguments: {'initialStatus': currentStatus},
);
break;
case 2: // 笔记标签页
case 2:
Navigator.pushNamed(context, '/note-form');
break;
default:
@@ -161,34 +154,33 @@ class CustomBottomNavBar extends StatelessWidget {
}
}
/// 显示新增对话框
void _showAddDialog(BuildContext context, AppProvider provider) {
final colors = Theme.of(context).colorScheme;
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
backgroundColor: colors.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (BuildContext context) {
final bottomColors = Theme.of(context).colorScheme;
return SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 顶部指示条
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: const Color(0xFFE0E0E0),
color: bottomColors.onSurface.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
// 标题
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
Text(
@@ -196,15 +188,15 @@ class CustomBottomNavBar extends StatelessWidget {
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: bottomColors.onSurface,
),
),
],
),
),
const SizedBox(height: 16),
// 选项列表
_buildAddOption(
colors: bottomColors,
icon: Icons.movie_outlined,
title: '添加观影',
subtitle: '记录你看过的电影',
@@ -224,6 +216,7 @@ class CustomBottomNavBar extends StatelessWidget {
},
),
_buildAddOption(
colors: bottomColors,
icon: Icons.menu_book_outlined,
title: '添加阅读',
subtitle: '记录你读过的书',
@@ -243,6 +236,7 @@ class CustomBottomNavBar extends StatelessWidget {
},
),
_buildAddOption(
colors: bottomColors,
icon: Icons.note_outlined,
title: '添加笔记',
subtitle: '记录你的想法和笔记',
@@ -259,8 +253,8 @@ class CustomBottomNavBar extends StatelessWidget {
);
}
/// 构建新增选项
Widget _buildAddOption({
required ColorScheme colors,
required IconData icon,
required String title,
required String subtitle,
@@ -276,13 +270,13 @@ class CustomBottomNavBar extends StatelessWidget {
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
size: 22,
color: const Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(width: 16),
@@ -292,26 +286,26 @@ class CustomBottomNavBar extends StatelessWidget {
children: [
Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
),
),
const Icon(
Icon(
Icons.chevron_right,
color: Color(0xFFCCCCCC),
color: colors.onSurface.withValues(alpha: 0.25),
size: 20,
),
],

View File

@@ -33,36 +33,25 @@ class _CustomDrawerState extends State<CustomDrawer> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Drawer(
backgroundColor: const Color(0xFFF8F8F8),
backgroundColor: colors.surfaceContainerHigh,
child: SafeArea(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 头像 + 统计(独立卡片区域)
_buildProfileCard(context),
const SizedBox(height: 16),
// 热力图
_buildCalendarSection(context),
const SizedBox(height: 16),
// 最近添加
_buildRecentSection(context),
const SizedBox(height: 16),
// 功能入口
_buildToolsCard(context),
// 底部版本号
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 32),
child: Center(
child: Text('v$_version', style: const TextStyle(fontSize: 11, color: Color(0xFFD0D0D0))),
child: Text('v$_version', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.2))),
),
),
],
@@ -77,6 +66,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
Widget _buildProfileCard(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
final colors = Theme.of(context).colorScheme;
final userPrefs = UserPrefs();
final nickname = userPrefs.nickname;
final motto = userPrefs.motto;
@@ -89,77 +79,69 @@ class _CustomDrawerState extends State<CustomDrawer> {
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 头像 + 名称/座右铭 + 统计
Row(
children: [
// 头像
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFF5F5F5),
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5),
color: colors.surfaceContainerHighest,
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
clipBehavior: Clip.antiAlias,
child: avatarPath != null && avatarPath.isNotEmpty
? Image.file(File(avatarPath), fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
const Icon(Icons.person_outline, size: 26, color: Color(0xFFBBBBBB)))
: const Icon(Icons.person_outline, size: 26, color: Color(0xFFBBBBBB)),
Icon(Icons.person_outline, size: 26, color: colors.onSurface.withValues(alpha: 0.3)))
: Icon(Icons.person_outline, size: 26, color: colors.onSurface.withValues(alpha: 0.3)),
),
const SizedBox(width: 14),
// 名称 + 座右铭
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(nickname, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
Text(nickname, style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
const SizedBox(height: 2),
Text(motto, maxLines: 1, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
],
),
),
],
),
const SizedBox(height: 16),
const Divider(height: 1, color: Color(0xFFF0F0F0)),
Divider(height: 1, color: colors.outlineVariant),
const SizedBox(height: 14),
// 统计数字
_buildProfileStatRow(Icons.movie_outlined, movieCount, '观影'),
const SizedBox(height: 12),
_buildProfileStatRow(Icons.menu_book_outlined, bookCount, '阅读'),
const SizedBox(height: 12),
_buildProfileStatRow(Icons.note_outlined, noteCount, '笔记'),
// 设置入口
const SizedBox(height: 14),
const Divider(height: 1, color: Color(0xFFF0F0F0)),
Divider(height: 1, color: colors.outlineVariant),
InkWell(
onTap: () {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsPage()));
},
borderRadius: BorderRadius.circular(8),
child: const Padding(
padding: EdgeInsets.only(top: 14),
child: Padding(
padding: const EdgeInsets.only(top: 14),
child: Row(
children: [
Icon(Icons.settings_outlined, size: 16, color: Color(0xFF999999)),
SizedBox(width: 8),
Text('设置', style: TextStyle(fontSize: 13, color: Color(0xFF888888))),
Spacer(),
Icon(Icons.chevron_right, size: 14, color: Color(0xFFD0D0D0)),
Icon(Icons.settings_outlined, size: 16, color: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 8),
Text('设置', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
const Spacer(),
Icon(Icons.chevron_right, size: 14, color: colors.onSurface.withValues(alpha: 0.2)),
],
),
),
@@ -172,13 +154,14 @@ class _CustomDrawerState extends State<CustomDrawer> {
}
Widget _buildProfileStatRow(IconData icon, int count, String label) {
final colors = Theme.of(context).colorScheme;
return Row(
children: [
Icon(icon, size: 16, color: const Color(0xFF888888)),
Icon(icon, size: 16, color: colors.onSurface.withValues(alpha: 0.5)),
const SizedBox(width: 10),
Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF888888))),
Text(label, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
const Spacer(),
Text(_formatCount(count), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
Text(_formatCount(count), style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
],
);
}
@@ -192,10 +175,11 @@ class _CustomDrawerState extends State<CustomDrawer> {
// ─── 功能入口卡片 ────────────────────────────────────────────────────
Widget _buildToolsCard(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(16),
),
child: Column(
@@ -204,12 +188,12 @@ class _CustomDrawerState extends State<CustomDrawer> {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage()));
}, topRounded: true),
const Divider(height: 1, indent: 52, endIndent: 20, color: Color(0xFFF0F0F0)),
Divider(height: 1, indent: 52, endIndent: 20, color: colors.outlineVariant),
_buildToolItem(Icons.label_outline, '标签管理', () {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (_) => const TagManagementPage()));
}),
const Divider(height: 1, indent: 52, endIndent: 20, color: Color(0xFFF0F0F0)),
Divider(height: 1, indent: 52, endIndent: 20, color: colors.outlineVariant),
_buildToolItem(Icons.description_outlined, 'MD阅读', () {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (_) => const MdReaderTabPage()));
@@ -220,6 +204,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
}
Widget _buildToolItem(IconData icon, String title, VoidCallback onTap, {bool topRounded = false, bool bottomRounded = false}) {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.only(
@@ -232,21 +217,22 @@ class _CustomDrawerState extends State<CustomDrawer> {
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20),
child: Row(
children: [
Icon(icon, size: 20, color: const Color(0xFF555555)),
Icon(icon, size: 20, color: colors.onSurface.withValues(alpha: 0.7)),
const SizedBox(width: 12),
Expanded(child: Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))),
const Icon(Icons.chevron_right, size: 16, color: Color(0xFFD0D0D0)),
Expanded(child: Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface))),
Icon(Icons.chevron_right, size: 16, color: colors.onSurface.withValues(alpha: 0.2)),
],
),
),
);
}
// ─── 热力图(保持现状) ──────────────────────────────────────────────
// ─── 热力图 ──────────────────────────────────────────────────────────
Widget _buildCalendarSection(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
final colors = Theme.of(context).colorScheme;
final Map<DateTime, int> dailyCounts = {};
for (final movie in provider.movies.where((m) => !m.isDeleted)) {
final date = DateTime(movie.createdAt.year, movie.createdAt.month, movie.createdAt.day);
@@ -299,15 +285,15 @@ class _CustomDrawerState extends State<CustomDrawer> {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(16)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
Row(
children: [
Icon(Icons.calendar_today, size: 14, color: Color(0xFF999999)),
SizedBox(width: 8),
Text('热力图', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))),
Icon(Icons.calendar_today, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 8),
Text('热力图', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6))),
],
),
const SizedBox(height: 14),
@@ -324,7 +310,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
final label = keepWeeks.contains(week) ? monthLabels[week] : null;
return SizedBox(
width: cellSize + cellGap,
child: label != null ? Text(label, style: const TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))) : null,
child: label != null ? Text(label, style: TextStyle(fontSize: 9, color: colors.onSurface.withValues(alpha: 0.3))) : null,
);
}),
),
@@ -347,7 +333,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Text('', style: TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))),
Text('', style: TextStyle(fontSize: 9, color: colors.onSurface.withValues(alpha: 0.3))),
const SizedBox(width: 3),
_legendCell(const Color(0xFFF0F0F0)),
_legendCell(const Color(0xFFC8E6C9)),
@@ -355,7 +341,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
_legendCell(const Color(0xFF2E7D32)),
_legendCell(const Color(0xFF1B5E20)),
const SizedBox(width: 3),
const Text('', style: TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))),
Text('', style: TextStyle(fontSize: 9, color: colors.onSurface.withValues(alpha: 0.3))),
],
),
],
@@ -384,21 +370,22 @@ class _CustomDrawerState extends State<CustomDrawer> {
Widget _buildRecentSection(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
final colors = Theme.of(context).colorScheme;
final recent = _getRecentItems(provider);
if (recent.isEmpty) return const SizedBox.shrink();
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(16)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
Row(
children: [
Icon(Icons.schedule, size: 14, color: Color(0xFF999999)),
SizedBox(width: 8),
Text('最近添加', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))),
Icon(Icons.schedule, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 8),
Text('最近添加', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6))),
],
),
const SizedBox(height: 14),
@@ -408,15 +395,15 @@ class _CustomDrawerState extends State<CustomDrawer> {
children: [
Icon(
item.type == 'movie' ? Icons.movie_outlined : item.type == 'book' ? Icons.menu_book_outlined : Icons.note_outlined,
size: 14, color: const Color(0xFFBBBBBB),
size: 14, color: colors.onSurface.withValues(alpha: 0.3),
),
const SizedBox(width: 10),
Expanded(
child: Text(item.title, maxLines: 1, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 13, color: Color(0xFF444444))),
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.75))),
),
const SizedBox(width: 8),
Text(_recentTimeAgo(item.date), style: const TextStyle(fontSize: 10, color: Color(0xFFCCCCCC))),
Text(_recentTimeAgo(item.date), style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.25))),
],
),
)),

View File

@@ -51,7 +51,6 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
return;
}
// 如果是 http 开头,直接当网络图片
if (widget.path!.startsWith('http')) {
_useNetwork = true;
_imageUrl = widget.path;
@@ -60,7 +59,6 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
return;
}
// 本地文件存在就直接显示
final file = File(widget.path!);
if (file.existsSync()) {
setState(() => _loaded = true);
@@ -68,7 +66,6 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
return;
}
// 本地不存在,尝试服务端 URL
if (ServerDataService.isActive) {
try {
final url = await ServerDataService.toImageUrl(widget.path!);
@@ -105,13 +102,14 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
if (_error) {
return widget.errorWidget ??
Container(
width: widget.width,
height: widget.height,
color: const Color(0xFFF5F5F5),
child: const Icon(Icons.broken_image_outlined, size: 24, color: Color(0xFFCCCCCC)),
color: colors.surfaceContainerHighest,
child: Icon(Icons.broken_image_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25)),
);
}
if (!_loaded) {
@@ -119,7 +117,7 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
Container(
width: widget.width,
height: widget.height,
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
);
}
return FadeTransition(
@@ -136,8 +134,8 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
Container(
width: widget.width,
height: widget.height,
color: const Color(0xFFF5F5F5),
child: const Icon(Icons.broken_image_outlined, size: 24, color: Color(0xFFCCCCCC)),
color: colors.surfaceContainerHighest,
child: Icon(Icons.broken_image_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25)),
);
},
)
@@ -150,8 +148,8 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
Container(
width: widget.width,
height: widget.height,
color: const Color(0xFFF5F5F5),
child: const Icon(Icons.broken_image_outlined, size: 24, color: Color(0xFFCCCCCC)),
color: colors.surfaceContainerHighest,
child: Icon(Icons.broken_image_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25)),
),
),
);

View File

@@ -15,6 +15,7 @@ class MovieListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: () {
Navigator.pushNamed(context, '/movie-detail', arguments: movie);
@@ -23,28 +24,21 @@ class MovieListItem extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 海报
Expanded(
child: _buildPoster(),
child: _buildPoster(colors),
),
const SizedBox(height: 8),
// 影视名称
Text(
movie.title,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
color: Theme.of(context).colorScheme.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
// 评分
if (movie.rating != null)
AnimatedStarRating(rating: movie.rating!, starSize: 12, showNumber: true)
else
@@ -53,45 +47,45 @@ class MovieListItem extends StatelessWidget {
),
);
}
/// 构建海报
Widget _buildPoster() {
Widget _buildPoster(ColorScheme colors) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5),
border: Border.all(color: colors.outline, width: 0.5),
),
clipBehavior: Clip.antiAlias,
child: FadeInLocalImage(
path: movie.posterPath,
fit: BoxFit.cover,
placeholder: const Center(child: Icon(Icons.movie_outlined, size: 24, color: Color(0xFFCCCCCC))),
errorWidget: const Center(child: Icon(Icons.movie_outlined, size: 24, color: Color(0xFFCCCCCC))),
placeholder: Center(child: Icon(Icons.movie_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25))),
errorWidget: Center(child: Icon(Icons.movie_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25))),
),
);
}
/// 显示删除确认对话框
void _showDeleteDialog(BuildContext context) {
final colors = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text(
title: Text(
'确认删除',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
content: Text(
'确定要删除《${movie.title}》吗?删除后可在回收站恢复。',
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.5,
),
),
@@ -99,7 +93,7 @@ class MovieListItem extends StatelessWidget {
TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF666666),
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('取消'),
@@ -111,8 +105,8 @@ class MovieListItem extends StatelessWidget {
ToastUtil.show(context, '已删除');
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
backgroundColor: colors.error,
foregroundColor: colors.onError,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),

View File

@@ -8,20 +8,21 @@ class MovieStatusBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Consumer<AppProvider>(
builder: (context, provider, child) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: const BoxDecoration(
color: Colors.white,
decoration: BoxDecoration(
color: colors.surface,
border: Border(
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
bottom: BorderSide(color: colors.outline, width: 0.5),
),
),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(24),
),
child: LayoutBuilder(
@@ -42,7 +43,7 @@ class MovieStatusBar extends StatelessWidget {
padding: const EdgeInsets.all(3),
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
color: colors.primary,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
@@ -58,18 +59,21 @@ class MovieStatusBar extends StatelessWidget {
Row(
children: [
_buildTab(
colors: colors,
label: '已看',
icon: Icons.check_circle_outline,
isSelected: provider.movieStatusIndex == 0,
onTap: () => provider.setMovieStatusIndex(0),
),
_buildTab(
colors: colors,
label: '在看',
icon: Icons.play_circle_outline,
isSelected: provider.movieStatusIndex == 1,
onTap: () => provider.setMovieStatusIndex(1),
),
_buildTab(
colors: colors,
label: '想看',
icon: Icons.bookmark_outline,
isSelected: provider.movieStatusIndex == 2,
@@ -89,6 +93,7 @@ class MovieStatusBar extends StatelessWidget {
}
Widget _buildTab({
required ColorScheme colors,
required String label,
required IconData icon,
required bool isSelected,
@@ -108,7 +113,7 @@ class MovieStatusBar extends StatelessWidget {
Icon(
icon,
size: 16,
color: isSelected ? Colors.white : const Color(0xFF666666),
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
),
const SizedBox(width: 6),
Text(
@@ -116,7 +121,7 @@ class MovieStatusBar extends StatelessWidget {
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? Colors.white : const Color(0xFF666666),
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
),
),
],

View File

@@ -14,14 +14,12 @@ class NoteListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 使用 RepaintBoundary 减少重绘
return RepaintBoundary(
child: _NoteListItemContent(note: note),
);
}
}
/// 笔记列表项内容 - 分离出来便于优化
class _NoteListItemContent extends StatelessWidget {
final Note note;
@@ -29,10 +27,10 @@ class _NoteListItemContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: () {
Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) async {
// 返回时刷新笔记列表
await context.read<AppProvider>().loadNotes();
});
},
@@ -41,40 +39,37 @@ class _NoteListItemContent extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 顶部:时间 + MD标记
Row(
children: [
// 时间
Text(
_formatDateCached(note.updatedAt),
style: const TextStyle(
style: TextStyle(
fontSize: 11,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(width: 8),
// MD标记
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(3),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: const Text(
child: Text(
'MD',
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
color: colors.onSurface.withValues(alpha: 0.4),
),
),
),
@@ -83,14 +78,13 @@ class _NoteListItemContent extends StatelessWidget {
const SizedBox(height: 6),
// 标题
if (note.title.isNotEmpty) ...[
Text(
note.title,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
color: colors.onSurface,
height: 1.4,
),
maxLines: 1,
@@ -99,20 +93,18 @@ class _NoteListItemContent extends StatelessWidget {
const SizedBox(height: 4),
],
// 内容摘要去除Markdown标记内容为空则不显示
if (_collapseBlankLines(_cleanMarkdown(note.content).trim()).isNotEmpty)
Text(
_collapseBlankLines(_cleanMarkdown(note.content).trim()),
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.5,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
// 底部标签
if (note.tags.isNotEmpty) ...[
const SizedBox(height: 6),
Wrap(
@@ -122,15 +114,15 @@ class _NoteListItemContent extends StatelessWidget {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: Colors.white,
color: colors.surface,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Text(
tag,
style: const TextStyle(
style: TextStyle(
fontSize: 11,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
),
),
);
@@ -138,10 +130,9 @@ class _NoteListItemContent extends StatelessWidget {
),
],
// 图片预览
if (note.images.isNotEmpty) ...[
const SizedBox(height: 8),
_buildImagePreviewRow(),
_buildImagePreviewRow(colors),
],
],
),
@@ -149,8 +140,7 @@ class _NoteListItemContent extends StatelessWidget {
);
}
/// 图片预览行
Widget _buildImagePreviewRow() {
Widget _buildImagePreviewRow(ColorScheme colors) {
final images = note.images;
final count = images.length.clamp(0, 3);
return Row(
@@ -162,14 +152,14 @@ class _NoteListItemContent extends StatelessWidget {
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
clipBehavior: Clip.antiAlias,
child: FadeInLocalImage(
path: images[i],
fit: BoxFit.cover,
errorWidget: Container(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
),
),
),
@@ -178,13 +168,13 @@ class _NoteListItemContent extends StatelessWidget {
width: 48,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
),
child: Center(
child: Text(
'+${images.length - 3}',
style: const TextStyle(fontSize: 12, color: Color(0xFF999999), fontWeight: FontWeight.w500),
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4), fontWeight: FontWeight.w500),
),
),
),
@@ -192,45 +182,44 @@ class _NoteListItemContent extends StatelessWidget {
);
}
/// 清理 Markdown 标记,提取纯文本
String _cleanMarkdown(String text) {
return text
.replaceAll(RegExp(r'^#+\s+', multiLine: true), '') // 标题
.replaceAll(RegExp(r'\*\*(.+?)\*\*'), r'$1') // 粗体
.replaceAll(RegExp(r'\*(.+?)\*'), r'$1') // 斜体
.replaceAll(RegExp(r'`(.+?)`'), r'$1') // 行内代码
.replaceAll(RegExp(r'^\s*[-*+]\s', multiLine: true), '') // 列表
.replaceAll(RegExp(r'^\s*>\s', multiLine: true), '') // 引用
.replaceAll(RegExp(r'\[([^\]]+)\]\([^)]+\)'), r'$1') // 链接
.replaceAll(RegExp(r'!\[([^\]]*)\]\([^)]+\)'), '') // 图片
.replaceAll(RegExp(r'^#+\s+', multiLine: true), '')
.replaceAll(RegExp(r'\*\*(.+?)\*\*'), r'$1')
.replaceAll(RegExp(r'\*(.+?)\*'), r'$1')
.replaceAll(RegExp(r'`(.+?)`'), r'$1')
.replaceAll(RegExp(r'^\s*[-*+]\s', multiLine: true), '')
.replaceAll(RegExp(r'^\s*>\s', multiLine: true), '')
.replaceAll(RegExp(r'\[([^\]]+)\]\([^)]+\)'), r'$1')
.replaceAll(RegExp(r'!\[([^\]]*)\]\([^)]+\)'), '')
.trim();
}
/// 合并连续空行为单行
String _collapseBlankLines(String text) {
return text.replaceAll(RegExp(r'\n\s*\n+'), '\n');
}
/// 显示删除确认对话框
void _showDeleteDialog(BuildContext context) {
final colors = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text(
title: Text(
'确认删除',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
content: const Text(
content: Text(
'确定要删除这条笔记吗?删除后可在回收站恢复。',
style: TextStyle(
fontSize: 14,
color: Color(0xFF666666),
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.5,
),
),
@@ -238,7 +227,7 @@ class _NoteListItemContent extends StatelessWidget {
TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF666666),
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('取消'),
@@ -250,8 +239,8 @@ class _NoteListItemContent extends StatelessWidget {
ToastUtil.show(context, '已删除');
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
backgroundColor: colors.error,
foregroundColor: colors.onError,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
@@ -267,28 +256,24 @@ class _NoteListItemContent extends StatelessWidget {
}
}
// 日期格式化缓存
final Map<DateTime, String> _dateFormatCache = {};
/// 格式化日期(带缓存)
String _formatDateCached(DateTime date) {
// 使用日期部分作为缓存键(忽略时分秒)
final cacheKey = DateTime(date.year, date.month, date.day);
if (_dateFormatCache.containsKey(cacheKey)) {
return _dateFormatCache[cacheKey]!;
}
final result = _formatDate(date);
_dateFormatCache[cacheKey] = result;
return result;
}
/// 格式化日期
String _formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays == 0) {
if (difference.inHours == 0) {
if (difference.inMinutes == 0) {

View File

@@ -42,6 +42,7 @@ class _ShimmerSkeletonState extends State<ShimmerSkeleton>
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
@@ -49,7 +50,7 @@ class _ShimmerSkeletonState extends State<ShimmerSkeleton>
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
color: const Color(0xFFE0E0E0).withValues(alpha: _animation.value),
color: colors.surfaceContainerHighest.withValues(alpha: _animation.value),
borderRadius: BorderRadius.circular(widget.borderRadius),
),
);
@@ -134,6 +135,7 @@ class NoteSkeletonList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return ListView.builder(
padding: const EdgeInsets.fromLTRB(12, 10, 12, 100),
itemCount: 6,
@@ -141,7 +143,7 @@ class NoteSkeletonList extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
),
child: const Column(