适配深色模式

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

View File

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

View File

@@ -50,8 +50,9 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar( appBar: AppBar(
title: Text(_isEditing ? '编辑摘抄' : '添加摘抄'), title: Text(_isEditing ? '编辑摘抄' : '添加摘抄'),
actions: [ actions: [
@@ -63,10 +64,10 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
height: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) )
: const Text( : Text(
'保存', '保存',
style: TextStyle( style: TextStyle(
color: Color(0xFF1A1A1A), color: colors.onSurface,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
@@ -82,15 +83,15 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
// 章节 // 章节
TextFormField( TextFormField(
controller: _chapterController, controller: _chapterController,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: '章节(可选)', labelText: '章节(可选)',
hintText: '例如第一章、第3节等', hintText: '例如第一章、第3节等',
border: OutlineInputBorder( border: const OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.zero,
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.zero,
borderSide: BorderSide(color: Color(0xFF1A1A1A)), borderSide: BorderSide(color: colors.primary),
), ),
), ),
), ),
@@ -101,16 +102,16 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
TextFormField( TextFormField(
controller: _contentController, controller: _contentController,
maxLines: 8, maxLines: 8,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: '摘抄内容', labelText: '摘抄内容',
hintText: '输入你想要摘抄的内容...', hintText: '输入你想要摘抄的内容...',
alignLabelWithHint: true, alignLabelWithHint: true,
border: OutlineInputBorder( border: const OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.zero,
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.zero,
borderSide: BorderSide(color: Color(0xFF1A1A1A)), borderSide: BorderSide(color: colors.primary),
), ),
), ),
validator: (value) { validator: (value) {
@@ -127,16 +128,16 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
TextFormField( TextFormField(
controller: _commentController, controller: _commentController,
maxLines: 5, maxLines: 5,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: '我的感悟(可选)', labelText: '我的感悟(可选)',
hintText: '记录你对这段内容的思考和感悟...', hintText: '记录你对这段内容的思考和感悟...',
alignLabelWithHint: true, alignLabelWithHint: true,
border: OutlineInputBorder( border: const OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.zero,
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar( appBar: AppBar(
title: const Text('摘抄'), title: const Text('摘抄'),
actions: [ actions: [
@@ -62,6 +63,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
} }
Widget _buildEmptyState() { Widget _buildEmptyState() {
final colors = Theme.of(context).colorScheme;
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -70,21 +72,21 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
width: 80, width: 80,
height: 80, height: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: const Icon( child: Icon(
Icons.format_quote_outlined, Icons.format_quote_outlined,
size: 40, size: 40,
color: Color(0xFFCCCCCC), color: colors.onSurface.withValues(alpha: 0.25),
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
const Text( Text(
'暂无摘抄', '暂无摘抄',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -93,15 +95,15 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), color: colors.primary,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Text( child: Text(
'添加记录', '添加记录',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.white, color: colors.onPrimary,
), ),
), ),
), ),
@@ -114,7 +116,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
/// 按章节分组摘抄数据 /// 按章节分组摘抄数据
Map<String, List<BookExcerpt>> _groupExcerptsByChapter() { Map<String, List<BookExcerpt>> _groupExcerptsByChapter() {
final Map<String, List<BookExcerpt>> groups = {}; final Map<String, List<BookExcerpt>> groups = {};
for (final excerpt in _excerpts) { for (final excerpt in _excerpts) {
final chapter = excerpt.chapter.isEmpty ? '未分类' : excerpt.chapter; final chapter = excerpt.chapter.isEmpty ? '未分类' : excerpt.chapter;
if (!groups.containsKey(chapter)) { if (!groups.containsKey(chapter)) {
@@ -122,19 +124,19 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
} }
groups[chapter]!.add(excerpt); groups[chapter]!.add(excerpt);
} }
// 每个章节内的摘抄按时间排序(新的在前) // 每个章节内的摘抄按时间排序(新的在前)
for (final chapter in groups.keys) { for (final chapter in groups.keys) {
groups[chapter]!.sort((a, b) => b.createdAt.compareTo(a.createdAt)); groups[chapter]!.sort((a, b) => b.createdAt.compareTo(a.createdAt));
} }
return groups; return groups;
} }
Widget _buildExcerptList() { Widget _buildExcerptList() {
final groups = _groupExcerptsByChapter(); final groups = _groupExcerptsByChapter();
final chapters = groups.keys.toList(); final chapters = groups.keys.toList();
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
itemCount: chapters.length, itemCount: chapters.length,
@@ -147,6 +149,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
} }
Widget _buildChapterSection(String chapter, List<BookExcerpt> excerpts) { Widget _buildChapterSection(String chapter, List<BookExcerpt> excerpts) {
final colors = Theme.of(context).colorScheme;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -154,18 +157,18 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
border: Border( border: Border(
left: BorderSide(color: Color(0xFF1A1A1A), width: 4), left: BorderSide(color: colors.primary, width: 4),
), ),
), ),
child: Text( child: Text(
chapter, chapter,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
), ),
@@ -178,11 +181,12 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
} }
Widget _buildExcerptItem(BookExcerpt excerpt) { Widget _buildExcerptItem(BookExcerpt excerpt) {
final colors = Theme.of(context).colorScheme;
return Container( return Container(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)), border: Border.all(color: colors.outline),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -192,9 +196,9 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
excerpt.content, excerpt.content,
maxLines: 3, maxLines: 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xFF1A1A1A), color: colors.onSurface,
height: 1.5, height: 1.5,
), ),
), ),
@@ -205,16 +209,16 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
), ),
child: Text( child: Text(
excerpt.comment, excerpt.comment,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Color(0xFF666666), color: colors.onSurface.withValues(alpha: 0.6),
), ),
), ),
), ),
@@ -227,29 +231,29 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
children: [ children: [
Text( Text(
_formatDate(excerpt.createdAt), _formatDate(excerpt.createdAt),
style: const TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
), ),
const Spacer(), const Spacer(),
// 编辑按钮 // 编辑按钮
GestureDetector( GestureDetector(
onTap: () => _navigateToEditExcerpt(excerpt), onTap: () => _navigateToEditExcerpt(excerpt),
child: const Icon( child: Icon(
Icons.edit_outlined, Icons.edit_outlined,
size: 16, size: 16,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
// 删除按钮 // 删除按钮
GestureDetector( GestureDetector(
onTap: () => _showDeleteDialog(excerpt), onTap: () => _showDeleteDialog(excerpt),
child: const Icon( child: Icon(
Icons.delete_outline, Icons.delete_outline,
size: 16, size: 16,
color: Colors.red, color: colors.error,
), ),
), ),
], ],
@@ -287,28 +291,31 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
void _showDeleteDialog(BookExcerpt excerpt) { void _showDeleteDialog(BookExcerpt excerpt) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) {
backgroundColor: Colors.white, final colors = Theme.of(context).colorScheme;
elevation: 0, return AlertDialog(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), backgroundColor: colors.surface,
title: const Text('确认删除'), elevation: 0,
content: const Text('确定要删除这条摘抄吗?'), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
actions: [ title: const Text('确认删除'),
TextButton( content: const Text('确定要删除这条摘抄吗?'),
onPressed: () => Navigator.pop(context), actions: [
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), TextButton(
), onPressed: () => Navigator.pop(context),
TextButton( child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
onPressed: () async { ),
await context.read<AppProvider>().removeBookExcerpt(excerpt.id); TextButton(
Navigator.pop(context); onPressed: () async {
_loadExcerpts(); await context.read<AppProvider>().removeBookExcerpt(excerpt.id);
ToastUtil.show(context, '已删除'); Navigator.pop(context);
}, _loadExcerpts();
child: const Text('删除', style: TextStyle(color: Colors.red)), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar( appBar: AppBar(
title: const Text('书评详情'), title: const Text('书评详情'),
actions: [ actions: [
@@ -70,9 +71,9 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
// 书评内容 // 书评内容
Text( Text(
_review.content, _review.content,
style: const TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Color(0xFF1A1A1A), color: colors.onSurface,
height: 1.8, height: 1.8,
), ),
), ),
@@ -82,7 +83,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
// 分隔线 // 分隔线
Container( Container(
height: 0.5, height: 0.5,
color: const Color(0xFFE5E5E5), color: colors.outline,
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -92,6 +93,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
icon: Icons.person_outline, icon: Icons.person_outline,
label: '书评人:', label: '书评人:',
value: _review.reviewer.isNotEmpty ? _review.reviewer : '匿名', value: _review.reviewer.isNotEmpty ? _review.reviewer : '匿名',
colors: colors,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -102,6 +104,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
icon: Icons.source_outlined, icon: Icons.source_outlined,
label: '来源:', label: '来源:',
value: _review.source, value: _review.source,
colors: colors,
), ),
if (_review.source.isNotEmpty) const SizedBox(height: 16), if (_review.source.isNotEmpty) const SizedBox(height: 16),
@@ -111,6 +114,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
icon: Icons.category_outlined, icon: Icons.category_outlined,
label: '类型:', label: '类型:',
value: _review.typeText, value: _review.typeText,
colors: colors,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -120,6 +124,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
icon: Icons.access_time, icon: Icons.access_time,
label: '时间:', label: '时间:',
value: _formatDate(_review.createdAt), value: _formatDate(_review.createdAt),
colors: colors,
), ),
], ],
), ),
@@ -132,29 +137,30 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
required IconData icon, required IconData icon,
required String label, required String label,
required String value, required String value,
required ColorScheme colors,
}) { }) {
return Row( return Row(
children: [ children: [
Icon( Icon(
icon, icon,
size: 20, size: 20,
color: const Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text(
label, label,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Text( child: Text(
value, value,
style: const TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
), ),

View File

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

View File

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

View File

@@ -23,21 +23,22 @@ class _BookSharePageState extends State<BookSharePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: colors.surfaceContainerHighest,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: colors.surface,
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)), icon: Icon(Icons.close, color: colors.onSurface),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
title: const Text( title: Text(
'分享海报', '分享海报',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
centerTitle: true, centerTitle: true,
@@ -50,12 +51,12 @@ class _BookSharePageState extends State<BookSharePage> {
height: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) )
: const Text( : Text(
'分享', '分享',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
), ),
@@ -76,13 +77,14 @@ class _BookSharePageState extends State<BookSharePage> {
/// 构建海报 Widget /// 构建海报 Widget
Widget _buildPosterWidget() { Widget _buildPosterWidget() {
final colors = Theme.of(context).colorScheme;
final book = widget.book; final book = widget.book;
final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty; final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty;
return Container( return Container(
width: 320, width: 320,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -117,10 +119,10 @@ class _BookSharePageState extends State<BookSharePage> {
// 书名 // 书名
Text( Text(
book.title, book.title,
style: const TextStyle( style: TextStyle(
fontSize: 22, fontSize: 22,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
@@ -129,9 +131,9 @@ class _BookSharePageState extends State<BookSharePage> {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
book.alternateTitles.join(' / '), book.alternateTitles.join(' / '),
style: const TextStyle( style: TextStyle(
fontSize: 14, 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 SizedBox(width: 4),
const Text( Text(
'/ 10', '/ 10',
style: TextStyle( style: TextStyle(
fontSize: 12, 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) if (book.authors.isNotEmpty)
_buildInfoRow('作者', book.authors.join(' / ')), _buildInfoRow('作者', book.authors.join(' / '), colors),
// 出版社 // 出版社
if (book.publisher != null && book.publisher!.isNotEmpty) if (book.publisher != null && book.publisher!.isNotEmpty)
_buildInfoRow('出版社', book.publisher!), _buildInfoRow('出版社', book.publisher!, colors),
// 出版时间 // 出版时间
if (book.publishDate != null) if (book.publishDate != null)
_buildInfoRow( _buildInfoRow(
'出版', '出版',
'${book.publishDate!.year}.${book.publishDate!.month.toString().padLeft(2, '0')}.${book.publishDate!.day.toString().padLeft(2, '0')}', '${book.publishDate!.year}.${book.publishDate!.month.toString().padLeft(2, '0')}.${book.publishDate!.day.toString().padLeft(2, '0')}',
colors,
), ),
// 类型 // 类型
if (book.genres.isNotEmpty) if (book.genres.isNotEmpty)
_buildInfoRow('类型', book.genres.join(' / ')), _buildInfoRow('类型', book.genres.join(' / '), colors),
// ISBN // ISBN
if (book.isbn != null && book.isbn!.isNotEmpty) if (book.isbn != null && book.isbn!.isNotEmpty)
_buildInfoRow('ISBN', book.isbn!), _buildInfoRow('ISBN', book.isbn!, colors),
const SizedBox(height: 16), const SizedBox(height: 16),
// 简介 // 简介
if (book.summary != null && book.summary!.isNotEmpty) ...[ if (book.summary != null && book.summary!.isNotEmpty) ...[
const Text( Text(
'简介', '简介',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
book.summary!, book.summary!,
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Color(0xFF666666), color: colors.onSurface.withValues(alpha: 0.6),
height: 1.6, height: 1.6,
), ),
maxLines: 5, maxLines: 5,
@@ -219,7 +222,7 @@ class _BookSharePageState extends State<BookSharePage> {
const SizedBox(height: 20), const SizedBox(height: 20),
// 底部标识 // 底部标识
const Divider(height: 1, color: Color(0xFFE8E8E8)), Divider(height: 1, color: colors.outline),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -227,14 +230,14 @@ class _BookSharePageState extends State<BookSharePage> {
Icon( Icon(
Icons.book_outlined, Icons.book_outlined,
size: 14, size: 14,
color: const Color(0xFF1A1A1A).withOpacity(0.5), color: colors.onSurface.withValues(alpha: 0.5),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
'来自 MookNote', '来自 MookNote',
style: TextStyle( style: TextStyle(
fontSize: 12, 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( return Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: Row( child: Row(
@@ -256,17 +259,17 @@ class _BookSharePageState extends State<BookSharePage> {
children: [ children: [
Text( Text(
'$label', '$label',
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
), ),
Expanded( Expanded(
child: Text( child: Text(
value, value,
style: const TextStyle( style: TextStyle(
fontSize: 13, 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> { class _BookTabPageState extends State<BookTabPage> {
int _layoutStyle = 0; // 0: 封面网格, 1: 列表 int _layoutStyle = 0;
bool _firstLoad = true; bool _firstLoad = true;
@override @override
@@ -42,6 +42,7 @@ class _BookTabPageState extends State<BookTabPage> {
} }
Widget _buildBookList(BuildContext context) { Widget _buildBookList(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Consumer<AppProvider>( return Consumer<AppProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
final statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'}; final statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'};
@@ -55,8 +56,8 @@ class _BookTabPageState extends State<BookTabPage> {
if (books.isEmpty) { if (books.isEmpty) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async => await provider.loadBooks(), onRefresh: () async => await provider.loadBooks(),
color: const Color(0xFF1A1A1A), color: colors.primary,
backgroundColor: Colors.white, backgroundColor: colors.surface,
child: ListView( child: ListView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
children: [_buildEmptyState(context, provider.bookStatusIndex)], children: [_buildEmptyState(context, provider.bookStatusIndex)],
@@ -73,10 +74,11 @@ class _BookTabPageState extends State<BookTabPage> {
} }
Widget _buildGridView(List books, AppProvider provider) { Widget _buildGridView(List books, AppProvider provider) {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async => await provider.loadBooks(), onRefresh: () async => await provider.loadBooks(),
color: const Color(0xFF1A1A1A), color: colors.primary,
backgroundColor: Colors.white, backgroundColor: colors.surface,
child: GridView.builder( child: GridView.builder(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 100), padding: const EdgeInsets.fromLTRB(16, 16, 16, 100),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
@@ -92,10 +94,11 @@ class _BookTabPageState extends State<BookTabPage> {
} }
Widget _buildListView(List books, AppProvider provider) { Widget _buildListView(List books, AppProvider provider) {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async => await provider.loadBooks(), onRefresh: () async => await provider.loadBooks(),
color: const Color(0xFF1A1A1A), color: colors.primary,
backgroundColor: Colors.white, backgroundColor: colors.surface,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100), padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
itemCount: books.length, itemCount: books.length,
@@ -105,6 +108,7 @@ class _BookTabPageState extends State<BookTabPage> {
} }
Widget _buildListCard(book) { Widget _buildListCard(book) {
final colors = Theme.of(context).colorScheme;
return GestureDetector( return GestureDetector(
onTap: () => Navigator.pushNamed(context, '/book-detail', arguments: book), onTap: () => Navigator.pushNamed(context, '/book-detail', arguments: book),
onLongPress: () => _showDeleteDialog(context, book), onLongPress: () => _showDeleteDialog(context, book),
@@ -112,23 +116,22 @@ class _BookTabPageState extends State<BookTabPage> {
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFAFAFA), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Row( child: Row(
children: [ children: [
// 封面缩略图
Container( Container(
width: 48, height: 64, width: 48, height: 64,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF0F0F0), color: colors.outlineVariant,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: book.coverPath != null && book.coverPath!.isNotEmpty child: book.coverPath != null && book.coverPath!.isNotEmpty
? Image.file(File(book.coverPath!), fit: BoxFit.cover, ? Image.file(File(book.coverPath!), fit: BoxFit.cover,
errorBuilder: (_, __, ___) => 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)))
: const Icon(Icons.menu_book_outlined, size: 22, color: Color(0xFFCCCCCC)), : Icon(Icons.menu_book_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
@@ -136,11 +139,11 @@ class _BookTabPageState extends State<BookTabPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(book.title, maxLines: 1, overflow: TextOverflow.ellipsis, 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) ...[ if (book.authors.isNotEmpty) ...[
const SizedBox(height: 3), const SizedBox(height: 3),
Text(book.authors.take(2).join(''), maxLines: 1, overflow: TextOverflow.ellipsis, 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), const SizedBox(height: 6),
if (book.rating != null) if (book.rating != null)
@@ -151,7 +154,7 @@ class _BookTabPageState extends State<BookTabPage> {
), ),
), ),
const SizedBox(width: 8), 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) { void _showDeleteDialog(BuildContext context, book) {
final colors = Theme.of(context).colorScheme;
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
backgroundColor: Colors.white, backgroundColor: colors.surface,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), 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}》吗?删除后可在回收站恢复。', 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: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx), onPressed: () => Navigator.pop(ctx),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
), ),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
@@ -179,7 +183,7 @@ class _BookTabPageState extends State<BookTabPage> {
Navigator.pop(ctx); Navigator.pop(ctx);
}, },
style: ElevatedButton.styleFrom( 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)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
), ),
@@ -193,11 +197,12 @@ class _BookTabPageState extends State<BookTabPage> {
Widget _buildSkeleton() { Widget _buildSkeleton() {
return _layoutStyle == 1 return _layoutStyle == 1
? MovieSkeletonGrid() // reuse same grid skeleton pattern ? MovieSkeletonGrid()
: const BookSkeletonGrid(); : const BookSkeletonGrid();
} }
Widget _buildEmptyState(BuildContext context, int statusIndex) { Widget _buildEmptyState(BuildContext context, int statusIndex) {
final colors = Theme.of(context).colorScheme;
final statusText = ['已读', '在读', '想读'][statusIndex]; final statusText = ['已读', '在读', '想读'][statusIndex];
return Center( return Center(
child: Column( child: Column(
@@ -205,11 +210,11 @@ class _BookTabPageState extends State<BookTabPage> {
children: [ children: [
Container( Container(
width: 80, height: 80, width: 80, height: 80,
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(20)), decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)),
child: const Icon(Icons.menu_book_outlined, size: 40, color: Color(0xFFCCCCCC)), child: Icon(Icons.menu_book_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
), ),
const SizedBox(height: 20), 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), const SizedBox(height: 24),
InkWell( InkWell(
onTap: () { onTap: () {
@@ -218,8 +223,8 @@ class _BookTabPageState extends State<BookTabPage> {
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(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

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

View File

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

View File

@@ -90,26 +90,30 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
void _showPermissionDeniedDialog() { void _showPermissionDeniedDialog() {
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) {
title: const Text('需要存储权限', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), final colors = Theme.of(ctx).colorScheme;
content: const Text( return AlertDialog(
'Android 11+ 需要在系统设置中授予"所有文件访问权限"才能读取目录中的 Markdown 文件。\n\n是否前往设置?', title: const Text('需要存储权限',
style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6), style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
), content: Text(
actions: [ 'Android 11+ 需要在系统设置中授予"所有文件访问权限"才能读取目录中的 Markdown 文件。\n\n是否前往设置?',
TextButton( style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6),
onPressed: () => Navigator.pop(ctx),
child: const Text('取消', style: TextStyle(color: Color(0xFF999999))),
), ),
TextButton( actions: [
onPressed: () { TextButton(
Navigator.pop(ctx); onPressed: () => Navigator.pop(ctx),
openAppSettings(); child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4))),
}, ),
child: const Text('前往设置', style: TextStyle(color: Color(0xFF1A1A1A))), 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)) { for (final entity in dir.listSync(recursive: true, followLinks: false)) {
if (entity is File) { if (entity is File) {
final lower = p.basename(entity.path).toLowerCase(); 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; return true;
} }
} }
@@ -135,13 +142,28 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
try { try {
final dir = Directory(dirPath); final dir = Directory(dirPath);
if (!dir.existsSync()) return false; 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; bool hasAnyFile = false;
for (final entity in dir.listSync(recursive: true, followLinks: false)) { for (final entity in dir.listSync(recursive: true, followLinks: false)) {
if (entity is File) { if (entity is File) {
hasAnyFile = true; hasAnyFile = true;
final lower = p.basename(entity.path).toLowerCase(); 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; return false;
} }
if (!imageExts.any((ext) => lower.endsWith(ext))) { if (!imageExts.any((ext) => lower.endsWith(ext))) {
@@ -193,9 +215,13 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
} }
} else if (entity is File) { } else if (entity is File) {
final lower = name.toLowerCase(); 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(); 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) { void _openFile(_FileEntry entry) {
Navigator.push(context, MaterialPageRoute( Navigator.push(context,
builder: (context) => MdViewerPage(filePath: entry.path), MaterialPageRoute(
)); builder: (context) => MdViewerPage(filePath: entry.path),
));
} }
void _showSettingsSheet() { void _showSettingsSheet() {
@@ -249,6 +276,7 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
borderRadius: BorderRadius.vertical(top: Radius.circular(16)), borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
), ),
builder: (ctx) { builder: (ctx) {
final colors = Theme.of(ctx).colorScheme;
return StatefulBuilder( return StatefulBuilder(
builder: (ctx, setLocalState) => Padding( builder: (ctx, setLocalState) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
@@ -256,24 +284,31 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
width: 36, height: 4, width: 36,
height: 4,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFDDDDDD), color: colors.onSurface.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
const Align( Align(
alignment: Alignment.centerLeft, 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), const SizedBox(height: 16),
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: const Text('显示空目录', style: TextStyle(fontSize: 14, color: Color(0xFF333333))), title: Text('显示空目录',
subtitle: const Text('关闭后隐藏无 Markdown 文件的目录', style: TextStyle(fontSize: 12, color: Color(0xFF999999))), 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, value: _showEmptyDirs,
activeColor: const Color(0xFF1A1A1A), activeTrackColor: colors.primary,
onChanged: (val) { onChanged: (val) {
setLocalState(() => _showEmptyDirs = val); setLocalState(() => _showEmptyDirs = val);
UserPrefs().setShowEmptyDirs(val); UserPrefs().setShowEmptyDirs(val);
@@ -281,13 +316,17 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
_loadDirectory(); _loadDirectory();
}, },
), ),
const Divider(height: 0.5, color: Color(0xFFF0F0F0)), Divider(height: 0.5, color: colors.outlineVariant),
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: const Text('显示纯图片目录', style: TextStyle(fontSize: 14, color: Color(0xFF333333))), title: Text('显示纯图片目录',
subtitle: const Text('关闭后隐藏只含图片、无 Markdown 的目录', style: TextStyle(fontSize: 12, color: Color(0xFF999999))), 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, value: _showImageOnlyDirs,
activeColor: const Color(0xFF1A1A1A), activeTrackColor: colors.primary,
onChanged: (val) { onChanged: (val) {
setLocalState(() => _showImageOnlyDirs = val); setLocalState(() => _showImageOnlyDirs = val);
UserPrefs().setShowImageOnlyDirs(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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: colors.surface,
elevation: 0, elevation: 0,
title: Text( title: Text(
_currentPath != null ? p.basename(_currentPath!) : 'Markdown 阅读', _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( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFF1A1A1A)), icon: Icon(Icons.arrow_back, color: colors.onSurface),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
actions: [ 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) if (_currentPath != null)
Padding( Padding(
padding: const EdgeInsets.only(right: 4), padding: const EdgeInsets.only(right: 4),
@@ -346,86 +371,109 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14), 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( 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, onPressed: _showSettingsSheet,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
], ],
), ),
body: _buildBody(), body: _buildBody(colors),
); );
} }
Widget _buildBody() { Widget _buildBody(ColorScheme colors) {
if (_currentPath == null) { if (_currentPath == null) {
return _buildWelcome(); return _buildWelcome(colors);
} }
if (_isLoading) { if (_isLoading) {
return const Center(child: CircularProgressIndicator(color: Color(0xFF1A1A1A))); return Center(child: CircularProgressIndicator(color: colors.primary));
} }
if (_error != null) { if (_error != null) {
return _buildError(); return _buildError(colors);
} }
return RefreshIndicator( return RefreshIndicator(
onRefresh: _loadDirectory, onRefresh: _loadDirectory,
color: const Color(0xFF1A1A1A), color: colors.primary,
backgroundColor: Colors.white, backgroundColor: colors.surface,
child: _entries.isEmpty ? _buildEmpty() : ListView.separated( child: _entries.isEmpty
padding: EdgeInsets.zero, ? _buildEmpty(colors)
itemCount: (_canGoBack ? 1 : 0) + _entries.length, : ListView.separated(
separatorBuilder: (_, __) => const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFF0F0F0)), padding: EdgeInsets.zero,
itemBuilder: (context, index) { itemCount: (_canGoBack ? 1 : 0) + _entries.length,
if (_canGoBack && index == 0) { separatorBuilder: (_, __) =>
return ListTile( Divider(height: 0.5, thickness: 0.5, color: colors.outlineVariant),
leading: Container( itemBuilder: (context, index) {
width: 36, height: 36, if (_canGoBack && index == 0) {
decoration: BoxDecoration( return ListTile(
color: const Color(0xFFF5F5F5), leading: Container(
borderRadius: BorderRadius.circular(8), width: 36,
), height: 36,
child: const Icon(Icons.arrow_upward, size: 18, color: Color(0xFF888888)), decoration: BoxDecoration(
), color: colors.surfaceContainerHighest,
title: const Text('..', style: TextStyle(fontSize: 14, color: Color(0xFF888888))), borderRadius: BorderRadius.circular(8),
onTap: _goBack, ),
); child: Icon(Icons.arrow_upward,
} size: 18, color: colors.onSurface.withValues(alpha: 0.5)),
final entry = _entries[_canGoBack ? index - 1 : index]; ),
return ListTile( title: Text('..',
leading: Container( style: TextStyle(
width: 36, height: 36, fontSize: 14, color: colors.onSurface.withValues(alpha: 0.5))),
decoration: BoxDecoration( onTap: _goBack,
color: entry.isDir ? const Color(0xFFF0F7FF) : const Color(0xFFF5F5F5), );
borderRadius: BorderRadius.circular(8), }
), final entry = _entries[_canGoBack ? index - 1 : index];
child: Icon( return ListTile(
entry.isDir ? Icons.folder_outlined : Icons.description_outlined, leading: Container(
size: 18, width: 36,
color: entry.isDir ? const Color(0xFF4A90D9) : const Color(0xFF666666), 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( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(40), padding: const EdgeInsets.all(40),
@@ -433,27 +481,34 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
width: 80, height: 80, width: 80,
height: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20), 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 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 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), const SizedBox(height: 32),
GestureDetector( GestureDetector(
onTap: _pickDirectory, onTap: _pickDirectory,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), color: colors.primary,
borderRadius: BorderRadius.circular(24), 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( return Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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 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), 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), const SizedBox(height: 24),
GestureDetector( GestureDetector(
onTap: _canGoBack ? _goBack : () => _pickDirectory(), onTap: _canGoBack ? _goBack : () => _pickDirectory(),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFDDDDDD)), border: Border.all(color: colors.onSurface.withValues(alpha: 0.15)),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Text( child: Text(
_canGoBack ? '返回上级目录' : '换一个目录', _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( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(40), padding: const EdgeInsets.all(40),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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), 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), 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), const SizedBox(height: 24),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -513,10 +575,11 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), color: colors.primary,
borderRadius: BorderRadius.circular(20), 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), const SizedBox(width: 12),
@@ -525,10 +588,12 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFDDDDDD)), border: Border.all(color: colors.onSurface.withValues(alpha: 0.15)),
borderRadius: BorderRadius.circular(20), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final fileName = widget.filePath.split('/').last; final fileName = widget.filePath.split('/').last;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,
title: Text( title: Text(
@@ -61,69 +62,71 @@ class _MdViewerPageState extends State<MdViewerPage> {
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600), style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
), ),
), ),
body: _buildBody(), body: _buildBody(colors),
); );
} }
Widget _buildBody() { Widget _buildBody(ColorScheme colors) {
if (_isLoading) { if (_isLoading) {
return const Center(child: CircularProgressIndicator(color: Color(0xFF1A1A1A))); return Center(child: CircularProgressIndicator(color: colors.primary));
} }
if (_error != null) { if (_error != null) {
return _buildErrorState(); return _buildErrorState(colors);
} }
return Markdown( return Markdown(
data: _content, data: _content,
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
styleSheet: MarkdownStyleSheet( styleSheet: MarkdownStyleSheet(
h1: const TextStyle( h1: TextStyle(
fontSize: 22, fontSize: 22,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
height: 1.4, height: 1.4,
), ),
h2: const TextStyle( h2: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
height: 1.4, height: 1.4,
), ),
h3: const TextStyle( h3: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
height: 1.4, height: 1.4,
), ),
p: const TextStyle( p: TextStyle(
fontSize: 15, fontSize: 15,
color: Color(0xFF1A1A1A), color: colors.onSurface,
height: 1.8, height: 1.8,
), ),
code: const TextStyle( code: TextStyle(
fontSize: 13, fontSize: 13,
color: Color(0xFF1A1A1A), color: colors.onSurface,
backgroundColor: Color(0xFFF5F5F5), backgroundColor: colors.surfaceContainerHighest,
), ),
codeblockDecoration: BoxDecoration( codeblockDecoration: BoxDecoration(
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
border: Border.all(color: const Color(0xFFE5E5E5)), border: Border.all(color: colors.outline),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
codeblockPadding: const EdgeInsets.all(12), codeblockPadding: const EdgeInsets.all(12),
blockquote: const TextStyle( blockquote: TextStyle(
fontSize: 15, fontSize: 15,
color: Color(0xFF666666), color: colors.onSurface.withValues(alpha: 0.6),
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
), ),
blockquoteDecoration: const BoxDecoration( blockquoteDecoration: BoxDecoration(
border: Border(left: BorderSide(color: Color(0xFF999999), width: 4)), border: Border(
left: BorderSide(
color: colors.onSurface.withValues(alpha: 0.4), width: 4)),
), ),
blockquotePadding: const EdgeInsets.only(left: 12), blockquotePadding: const EdgeInsets.only(left: 12),
listBullet: const TextStyle( listBullet: TextStyle(
fontSize: 15, fontSize: 15,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
listIndent: 24, listIndent: 24,
a: const TextStyle( a: const TextStyle(
@@ -133,12 +136,12 @@ class _MdViewerPageState extends State<MdViewerPage> {
), ),
), ),
// ignore: deprecated_member_use // 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(); if (uri.isEmpty) return const SizedBox.shrink();
// 处理相对路径:基于 md 文件所在目录 // 处理相对路径:基于 md 文件所在目录
@@ -158,17 +161,19 @@ class _MdViewerPageState extends State<MdViewerPage> {
return Container( return Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Row( child: Row(
children: [ 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), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
alt ?? '图片加载失败', 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( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), const SizedBox(height: 16),
Text( Text(
_error!, _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页面 - 用于抓取影视信息 /// 豆瓣影视WebView页面 - 用于抓取影视信息
class DoubanWebViewPage extends StatefulWidget { class DoubanWebViewPage extends StatefulWidget {
final String url; final String url;
const DoubanWebViewPage({super.key, required this.url}); const DoubanWebViewPage({super.key, required this.url});
@override @override
State<DoubanWebViewPage> createState() => _DoubanWebViewPageState(); State<DoubanWebViewPage> createState() => _DoubanWebViewPageState();
} }
@@ -16,21 +16,21 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
late WebViewController _controller; late WebViewController _controller;
bool _isLoading = true; bool _isLoading = true;
bool _canExtract = false; bool _canExtract = false;
bool _isExtracting = false; // 防止重复提取 bool _isExtracting = false; // 防止重复提取
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initWebView(); _initWebView();
} }
@override @override
void dispose() { void dispose() {
// 清理 WebView 资源 // 清理 WebView 资源
_controller.loadRequest(Uri.parse('about:blank')); _controller.loadRequest(Uri.parse('about:blank'));
super.dispose(); super.dispose();
} }
void _initWebView() { void _initWebView() {
_controller = WebViewController() _controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted) ..setJavaScriptMode(JavaScriptMode.unrestricted)
@@ -55,23 +55,26 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
) )
..loadRequest(Uri.parse(widget.url)); ..loadRequest(Uri.parse(widget.url));
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar( appBar: AppBar(
title: const Text('豆瓣影视'), title: const Text('豆瓣影视'),
leading: _buildBackButton(), leading: _buildBackButton(),
actions: [ actions: [
// 提取按钮 - 始终显示 // 提取按钮 - 始终显示
_buildActionButton( _buildActionButton(
colors: colors,
icon: Icons.auto_fix_high_outlined, icon: Icons.auto_fix_high_outlined,
onPressed: _showExtractedInfo, onPressed: _showExtractedInfo,
tooltip: '提取信息', tooltip: '提取信息',
), ),
// 刷新按钮 // 刷新按钮
_buildActionButton( _buildActionButton(
colors: colors,
icon: Icons.refresh, icon: Icons.refresh,
onPressed: () => _controller.reload(), onPressed: () => _controller.reload(),
tooltip: '刷新', tooltip: '刷新',
@@ -83,16 +86,14 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
children: [ children: [
WebViewWidget(controller: _controller), WebViewWidget(controller: _controller),
// 加载指示器 // 加载指示器
if (_isLoading) if (_isLoading) const Center(
const Center( child: CircularProgressIndicator(),
child: CircularProgressIndicator(), ),
),
], ],
), ),
); );
} }
/// 构建返回按钮 /// 构建返回按钮
Widget _buildBackButton() { Widget _buildBackButton() {
return Container( return Container(
@@ -121,6 +122,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
/// 构建右上角操作按钮 /// 构建右上角操作按钮
Widget _buildActionButton({ Widget _buildActionButton({
required ColorScheme colors,
required IconData icon, required IconData icon,
required VoidCallback onPressed, required VoidCallback onPressed,
required String tooltip, required String tooltip,
@@ -138,75 +140,86 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Container( child: Container(
padding: const EdgeInsets.all(8), 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 { Future<void> _showExtractedInfo() async {
// 先提取信息 // 先提取信息
final movieInfo = await _extractMovieInfo(); final movieInfo = await _extractMovieInfo();
if (movieInfo == null) return; if (movieInfo == null) return;
// 显示提取的信息 // 显示提取的信息
if (mounted) { if (mounted) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (ctx) {
backgroundColor: Colors.white, final colors = Theme.of(ctx).colorScheme;
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), return AlertDialog(
title: const Text( backgroundColor: colors.surface,
'提取的影视信息', shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
style: TextStyle( title: Text(
fontSize: 18, '提取的影视信息',
fontWeight: FontWeight.w600, style: TextStyle(
color: Color(0xFF1A1A1A), fontSize: 18,
), fontWeight: FontWeight.w600,
), color: colors.onSurface,
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)),
), ),
), ),
TextButton( content: SingleChildScrollView(
onPressed: () { child: Column(
Navigator.pop(context); mainAxisSize: MainAxisSize.min,
Navigator.pop(context, movieInfo); crossAxisAlignment: CrossAxisAlignment.start,
}, children: [
child: const Text( _buildInfoRow(colors, '标题', movieInfo['title']?.toString() ?? '未提取到'),
'使用此信息', _buildInfoRow(colors, '导演', movieInfo['director']?.toString() ?? '未提取到'),
style: TextStyle(color: Color(0xFF1A1A1A), fontWeight: FontWeight.w600), _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( return Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: Row( child: Row(
@@ -216,18 +229,18 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
width: 64, width: 64,
child: Text( child: Text(
label, label,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
), ),
), ),
Expanded( Expanded(
child: Text( child: Text(
value, value,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
), ),
@@ -240,10 +253,10 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
Future<Map<String, dynamic>?> _extractMovieInfo() async { Future<Map<String, dynamic>?> _extractMovieInfo() async {
// 检查是否已提取过,避免重复点击 // 检查是否已提取过,避免重复点击
if (_isExtracting) return null; if (_isExtracting) return null;
try { try {
_isExtracting = true; _isExtracting = true;
// 显示加载提示 // 显示加载提示
showDialog( showDialog(
context: context, context: context,
@@ -252,16 +265,16 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
); );
// 执行JavaScript代码提取页面信息 // 执行JavaScript代码提取页面信息
final result = await _controller.runJavaScriptReturningResult(r''' final result = await _controller.runJavaScriptReturningResult(r'''
(function() { (function() {
const info = {}; const info = {};
// 获取标题 - 移动版页面 // 获取标题 - 移动版页面
const titleEl = document.querySelector('.sub-title'); const titleEl = document.querySelector('.sub-title');
info.title = titleEl ? titleEl.textContent.trim() : ''; info.title = titleEl ? titleEl.textContent.trim() : '';
// 获取年份 - 从 original-title 中提取 // 获取年份 - 从 original-title 中提取
const originalTitleEl = document.querySelector('.sub-original-title'); const originalTitleEl = document.querySelector('.sub-original-title');
if (originalTitleEl) { if (originalTitleEl) {
@@ -270,7 +283,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} else { } else {
info.year = ''; info.year = '';
} }
// 获取封面图 - 从 sub-cover 中的 img 标签获取 // 获取封面图 - 从 sub-cover 中的 img 标签获取
const coverEl = document.querySelector('.sub-cover img'); const coverEl = document.querySelector('.sub-cover img');
if (coverEl) { if (coverEl) {
@@ -283,11 +296,11 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} else { } else {
info.coverUrl = ''; info.coverUrl = '';
} }
// 获取评分 - 移动版可能在 mark-item 中 // 获取评分 - 移动版可能在 mark-item 中
const ratingEl = document.querySelector('.rating-num') || document.querySelector('.score'); const ratingEl = document.querySelector('.rating-num') || document.querySelector('.score');
info.rating = ratingEl ? ratingEl.textContent.trim() : ''; info.rating = ratingEl ? ratingEl.textContent.trim() : '';
// 获取导演 - 从演职员列表中找 // 获取导演 - 从演职员列表中找
const directorEl = document.querySelector('.movie-celebrities .item__celebrity .role'); const directorEl = document.querySelector('.movie-celebrities .item__celebrity .role');
if (directorEl && directorEl.textContent.includes('导演')) { if (directorEl && directorEl.textContent.includes('导演')) {
@@ -296,7 +309,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} else { } else {
info.director = ''; info.director = '';
} }
// 获取编剧 - 从演职员列表中找(匹配"编剧"或"剧本" // 获取编剧 - 从演职员列表中找(匹配"编剧"或"剧本"
const writerEls = document.querySelectorAll('.movie-celebrities .item__celebrity'); const writerEls = document.querySelectorAll('.movie-celebrities .item__celebrity');
const writers = []; const writers = [];
@@ -308,7 +321,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} }
}); });
info.writers = writers; info.writers = writers;
// 获取主演- 从演职员列表中找前5个 // 获取主演- 从演职员列表中找前5个
const actorEls = document.querySelectorAll('.movie-celebrities .item__celebrity'); const actorEls = document.querySelectorAll('.movie-celebrities .item__celebrity');
const actors = []; const actors = [];
@@ -316,10 +329,10 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
const roleEl = el const roleEl = el
.querySelector('.role'); .querySelector('.role');
if (roleEl && ( 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('参演') ||
roleEl.textContent.includes('饰') roleEl.textContent.includes('饰')
)) { )) {
const nameEl = el.querySelector('.name'); const nameEl = el.querySelector('.name');
@@ -327,20 +340,20 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} }
}); });
info.actors = actors; info.actors = actors;
// 获取类型 - 从 sub-meta 或标签中提取 // 获取类型 - 从 sub-meta 或标签中提取
const metaEl = document.querySelector('.sub-meta'); const metaEl = document.querySelector('.sub-meta');
if (metaEl) { if (metaEl) {
const metaText = metaEl.textContent; const metaText = metaEl.textContent;
const parts = metaText.split('/').map(s => s.trim()); 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('上映') p && !p.match(/^\d{4}/) && !p.includes('分钟') && !p.includes('上映')
).join(','); ).join(',');
} else { } else {
info.genres = ''; info.genres = '';
} }
// 获取上映日期 // 获取上映日期
if (metaEl) { if (metaEl) {
const dateMatch = metaEl.textContent.match(/(\d{4}-\d{2}-\d{2})/); const dateMatch = metaEl.textContent.match(/(\d{4}-\d{2}-\d{2})/);
@@ -348,7 +361,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} else { } else {
info.releaseDate = ''; info.releaseDate = '';
} }
// 获取简介 // 获取简介
const summaryEl = document.querySelector('.subject-intro p'); const summaryEl = document.querySelector('.subject-intro p');
if (summaryEl) { if (summaryEl) {
@@ -356,7 +369,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} else { } else {
info.summary = ''; info.summary = '';
} }
// 获取别名 - 从 original-title 中提取(去掉年份) // 获取别名 - 从 original-title 中提取(去掉年份)
if (originalTitleEl) { if (originalTitleEl) {
const fullText = originalTitleEl.textContent.trim(); const fullText = originalTitleEl.textContent.trim();
@@ -364,14 +377,14 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
} else { } else {
info.alternateTitles = []; info.alternateTitles = [];
} }
return JSON.stringify(info); return JSON.stringify(info);
})() })()
'''); ''');
// 关闭加载提示 // 关闭加载提示
if (mounted) Navigator.pop(context); if (mounted) Navigator.pop(context);
// 解析提取的信息 // 解析提取的信息
// result 是 JavaScript 执行结果,已经是 JSON 字符串(带引号的) // result 是 JavaScript 执行结果,已经是 JSON 字符串(带引号的)
final String jsonStr = result.toString(); final String jsonStr = result.toString();
@@ -380,12 +393,12 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
? jsonDecode(jsonStr) as String ? jsonDecode(jsonStr) as String
: jsonStr; : jsonStr;
final Map<String, dynamic> movieInfo = jsonDecode(cleanJson) as Map<String, dynamic>; final Map<String, dynamic> movieInfo = jsonDecode(cleanJson) as Map<String, dynamic>;
return movieInfo; return movieInfo;
} catch (e) { } catch (e) {
// 关闭加载提示 // 关闭加载提示
if (mounted) Navigator.pop(context); if (mounted) Navigator.pop(context);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('提取信息失败: $e')), SnackBar(content: Text('提取信息失败: $e')),

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -46,10 +46,11 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final isEdit = widget.review != null; final isEdit = widget.review != null;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar( appBar: AppBar(
title: Text(isEdit ? '编辑影评' : '写影评'), title: Text(isEdit ? '编辑影评' : '写影评'),
actions: [ actions: [
@@ -73,24 +74,24 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
// 顶部信息栏 // 顶部信息栏
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: const BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), bottom: BorderSide(color: colors.outline, width: 0.5),
), ),
), ),
child: Row( child: Row(
children: [ children: [
// 类型选择 // 类型选择
_buildTypeSelector(), _buildTypeSelector(colors),
const SizedBox(width: 16), const SizedBox(width: 16),
// 评论人 // 评论人
Expanded( Expanded(
child: TextField( child: TextField(
controller: _reviewerController, controller: _reviewerController,
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)), style: TextStyle(fontSize: 14, color: colors.onSurface),
decoration: const InputDecoration( decoration: InputDecoration(
hintText: '评论人', hintText: '评论人',
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)), hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
border: InputBorder.none, border: InputBorder.none,
isDense: true, isDense: true,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
@@ -103,10 +104,10 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
width: 100, width: 100,
child: TextField( child: TextField(
controller: _sourceController, controller: _sourceController,
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)), style: TextStyle(fontSize: 14, color: colors.onSurface),
decoration: const InputDecoration( decoration: InputDecoration(
hintText: '来源', hintText: '来源',
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)), hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
border: InputBorder.none, border: InputBorder.none,
isDense: true, isDense: true,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
@@ -116,7 +117,7 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
], ],
), ),
), ),
// 评论内容区域 // 评论内容区域
Expanded( Expanded(
child: TextFormField( child: TextFormField(
@@ -124,19 +125,19 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
maxLines: null, maxLines: null,
expands: true, expands: true,
textAlignVertical: TextAlignVertical.top, textAlignVertical: TextAlignVertical.top,
style: const TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Color(0xFF1A1A1A), color: colors.onSurface,
height: 1.7, height: 1.7,
), ),
decoration: const InputDecoration( decoration: InputDecoration(
hintText: '写下你的影评...', hintText: '写下你的影评...',
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 16, fontSize: 16,
color: Color(0xFFCCCCCC), color: colors.onSurface.withValues(alpha: 0.25),
), ),
border: InputBorder.none, border: InputBorder.none,
contentPadding: EdgeInsets.all(16), contentPadding: const EdgeInsets.all(16),
), ),
validator: (value) { validator: (value) {
if (value == null || value.trim().isEmpty) { if (value == null || value.trim().isEmpty) {
@@ -153,29 +154,29 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
} }
/// 构建类型选择器 /// 构建类型选择器
Widget _buildTypeSelector() { Widget _buildTypeSelector(ColorScheme colors) {
return GestureDetector( return GestureDetector(
onTap: () => _showTypeSelector(), onTap: () => _showTypeSelector(),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)), border: Border.all(color: colors.outline),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
_reviewType == 1 ? '短评' : '长评', _reviewType == 1 ? '短评' : '长评',
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Color(0xFF666666), color: colors.onSurface.withValues(alpha: 0.6),
), ),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
const Icon( Icon(
Icons.arrow_drop_down, Icons.arrow_drop_down,
size: 16, size: 16,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
], ],
), ),
@@ -187,36 +188,42 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
void _showTypeSelector() { void _showTypeSelector() {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
backgroundColor: Colors.white, backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
builder: (context) => SafeArea( builder: (context) {
child: Column( final colors = Theme.of(context).colorScheme;
mainAxisSize: MainAxisSize.min, return Container(
children: [ color: colors.surface,
ListTile( child: SafeArea(
title: const Text('短评'), child: Column(
trailing: _reviewType == 1 mainAxisSize: MainAxisSize.min,
? const Icon(Icons.check, color: Color(0xFF1A1A1A)) children: [
: null, ListTile(
onTap: () { title: const Text('短评'),
setState(() => _reviewType = 1); trailing: _reviewType == 1
Navigator.pop(context); ? 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar( appBar: AppBar(
title: _isSearching title: _isSearching
? TextField( ? TextField(
controller: _searchController, controller: _searchController,
autofocus: true, autofocus: true,
decoration: const InputDecoration( decoration: InputDecoration(
hintText: '搜索影评内容、评论人、来源...', hintText: '搜索影评内容、评论人、来源...',
hintStyle: TextStyle(color: Color(0xFF999999)), hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)),
border: InputBorder.none, border: InputBorder.none,
), ),
style: const TextStyle(color: Color(0xFF1A1A1A)), style: TextStyle(color: colors.onSurface),
onChanged: _onSearchChanged, onChanged: _onSearchChanged,
) )
: const Text('影评'), : const Text('影评'),
@@ -112,6 +113,7 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
} }
Widget _buildEmptyState() { Widget _buildEmptyState() {
final colors = Theme.of(context).colorScheme;
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -120,21 +122,21 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
width: 80, width: 80,
height: 80, height: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: const Icon( child: Icon(
Icons.rate_review_outlined, Icons.rate_review_outlined,
size: 40, size: 40,
color: Color(0xFFCCCCCC), color: colors.onSurface.withValues(alpha: 0.25),
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
const Text( Text(
'暂无影评', '暂无影评',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -143,15 +145,15 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), color: colors.primary,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Text( child: Text(
'添加记录', '添加记录',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.white, color: colors.onPrimary,
), ),
), ),
), ),
@@ -176,13 +178,14 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
} }
Widget _buildReviewCard(MovieReview review) { Widget _buildReviewCard(MovieReview review) {
final colors = Theme.of(context).colorScheme;
return InkWell( return InkWell(
onTap: () => _navigateToReviewDetail(review), onTap: () => _navigateToReviewDetail(review),
onLongPress: () => _showDeleteDialog(review), onLongPress: () => _showDeleteDialog(review),
child: Container( child: Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Column( child: Column(
@@ -193,8 +196,8 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: review.reviewType == 1 color: review.reviewType == 1
? Colors.white ? colors.surface
: const Color(0xFF1A1A1A), : colors.primary,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: Text( child: Text(
@@ -202,8 +205,8 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: review.reviewType == 1 color: review.reviewType == 1
? const Color(0xFF666666) ? colors.onSurface.withValues(alpha: 0.6)
: Colors.white, : colors.onPrimary,
), ),
), ),
), ),
@@ -215,9 +218,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
review.content, review.content,
maxLines: review.reviewType == 1 ? 4 : 8, maxLines: review.reviewType == 1 ? 4 : 8,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Color(0xFF1A1A1A), color: colors.onSurface,
height: 1.5, height: 1.5,
), ),
), ),
@@ -232,9 +235,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
Expanded( Expanded(
child: Text( child: Text(
review.reviewer, review.reviewer,
style: const TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Color(0xFF666666), color: colors.onSurface.withValues(alpha: 0.6),
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -252,9 +255,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
Expanded( Expanded(
child: Text( child: Text(
review.source, review.source,
style: const TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -262,9 +265,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
// 日期 // 日期
Text( Text(
_formatDate(review.createdAt), _formatDate(review.createdAt),
style: const TextStyle( style: TextStyle(
fontSize: 10, 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) { void _showDeleteDialog(MovieReview review) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) {
backgroundColor: Colors.white, final colors = Theme.of(context).colorScheme;
elevation: 0, return AlertDialog(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), backgroundColor: colors.surface,
title: const Text('确认删除'), elevation: 0,
content: const Text('确定要删除这条影评吗?'), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
actions: [ title: const Text('确认删除'),
TextButton( content: const Text('确定要删除这条影评吗?'),
onPressed: () => Navigator.pop(context), actions: [
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), TextButton(
), onPressed: () => Navigator.pop(context),
TextButton( child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
onPressed: () async { ),
await context.read<AppProvider>().removeMovieReview(review.id); TextButton(
Navigator.pop(context); onPressed: () async {
_loadReviews(); await context.read<AppProvider>().removeMovieReview(review.id);
ToastUtil.show(context, '已删除'); Navigator.pop(context);
}, _loadReviews();
child: const Text('删除', style: TextStyle(color: Colors.red)), ToastUtil.show(context, '删除');
), },
], child: Text('删除', style: TextStyle(color: colors.error)),
), ),
],
);
},
); );
} }
} }

View File

@@ -24,21 +24,22 @@ class _MovieSharePageState extends State<MovieSharePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: colors.surfaceContainerHighest,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: colors.surface,
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)), icon: Icon(Icons.close, color: colors.onSurface),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
title: const Text( title: Text(
'分享海报', '分享海报',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
centerTitle: true, centerTitle: true,
@@ -51,12 +52,12 @@ class _MovieSharePageState extends State<MovieSharePage> {
height: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) )
: const Text( : Text(
'分享', '分享',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
), ),
@@ -77,13 +78,14 @@ class _MovieSharePageState extends State<MovieSharePage> {
/// 构建海报 Widget /// 构建海报 Widget
Widget _buildPosterWidget() { Widget _buildPosterWidget() {
final colors = Theme.of(context).colorScheme;
final movie = widget.movie; final movie = widget.movie;
final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty; final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty;
return Container( return Container(
width: 320, width: 320,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -118,10 +120,10 @@ class _MovieSharePageState extends State<MovieSharePage> {
// 标题 // 标题
Text( Text(
movie.title, movie.title,
style: const TextStyle( style: TextStyle(
fontSize: 22, fontSize: 22,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
@@ -130,9 +132,9 @@ class _MovieSharePageState extends State<MovieSharePage> {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
movie.alternateTitles.join(' / '), movie.alternateTitles.join(' / '),
style: const TextStyle( style: TextStyle(
fontSize: 14, 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 SizedBox(width: 4),
const Text( Text(
'/ 10', '/ 10',
style: TextStyle( style: TextStyle(
fontSize: 12, 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) if (movie.directors.isNotEmpty)
_buildInfoRow('导演', movie.directors.join(' / ')), _buildInfoRow('导演', movie.directors.join(' / '), colors),
// 编剧 // 编剧
if (movie.writers.isNotEmpty) if (movie.writers.isNotEmpty)
_buildInfoRow('编剧', movie.writers.join(' / ')), _buildInfoRow('编剧', movie.writers.join(' / '), colors),
// 主演 // 主演
if (movie.actors.isNotEmpty) if (movie.actors.isNotEmpty)
_buildInfoRow('主演', movie.actors.take(3).join(' / ')), _buildInfoRow('主演', movie.actors.take(3).join(' / '), colors),
// 类型 // 类型
if (movie.genres.isNotEmpty) if (movie.genres.isNotEmpty)
_buildInfoRow('类型', movie.genres.join(' / ')), _buildInfoRow('类型', movie.genres.join(' / '), colors),
// 上映日期 // 上映日期
if (movie.releaseDate != null) if (movie.releaseDate != null)
_buildInfoRow( _buildInfoRow(
'上映', '上映',
'${movie.releaseDate!.year}.${movie.releaseDate!.month.toString().padLeft(2, '0')}.${movie.releaseDate!.day.toString().padLeft(2, '0')}', '${movie.releaseDate!.year}.${movie.releaseDate!.month.toString().padLeft(2, '0')}.${movie.releaseDate!.day.toString().padLeft(2, '0')}',
colors,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// 简介 // 简介
if (movie.summary != null && movie.summary!.isNotEmpty) ...[ if (movie.summary != null && movie.summary!.isNotEmpty) ...[
const Text( Text(
'简介', '简介',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
movie.summary!, movie.summary!,
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Color(0xFF666666), color: colors.onSurface.withValues(alpha: 0.6),
height: 1.6, height: 1.6,
), ),
maxLines: 5, maxLines: 5,
@@ -220,7 +223,7 @@ class _MovieSharePageState extends State<MovieSharePage> {
const SizedBox(height: 20), const SizedBox(height: 20),
// 底部标识 // 底部标识
const Divider(height: 1, color: Color(0xFFE8E8E8)), Divider(height: 1, color: colors.outline),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -228,14 +231,14 @@ class _MovieSharePageState extends State<MovieSharePage> {
Icon( Icon(
Icons.movie_outlined, Icons.movie_outlined,
size: 14, size: 14,
color: const Color(0xFF1A1A1A).withOpacity(0.5), color: colors.onSurface.withValues(alpha: 0.5),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
'来自 MookNote', '来自 MookNote',
style: TextStyle( style: TextStyle(
fontSize: 12, 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( return Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: Row( child: Row(
@@ -257,17 +260,17 @@ class _MovieSharePageState extends State<MovieSharePage> {
children: [ children: [
Text( Text(
'$label', '$label',
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
), ),
Expanded( Expanded(
child: Text( child: Text(
value, value,
style: const TextStyle( style: TextStyle(
fontSize: 13, 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> { class _MovieTabPageState extends State<MovieTabPage> {
int _layoutStyle = 0; // 0: 海报网格, 1: 列表 int _layoutStyle = 0;
bool _firstLoad = true; bool _firstLoad = true;
@override @override
@@ -31,10 +31,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Column( return Column(
children: [ children: [
const MovieStatusBar(), const MovieStatusBar(),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), Divider(height: 0.5, thickness: 0.5, color: colors.outline),
Expanded( Expanded(
child: _buildMovieList(context), child: _buildMovieList(context),
), ),
@@ -43,12 +44,12 @@ class _MovieTabPageState extends State<MovieTabPage> {
} }
Widget _buildMovieList(BuildContext context) { Widget _buildMovieList(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Consumer<AppProvider>( return Consumer<AppProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
final statusMap = {0: 'watched', 1: 'watching', 2: 'want_to_watch'}; final statusMap = {0: 'watched', 1: 'watching', 2: 'want_to_watch'};
final currentStatus = statusMap[provider.movieStatusIndex]!; final currentStatus = statusMap[provider.movieStatusIndex]!;
final allMovies = provider.movies.where((m) => !m.isDeleted).toList(); final allMovies = provider.movies.where((m) => !m.isDeleted).toList();
// 首次加载且数据为空时才显示骨架屏
if (_firstLoad && allMovies.isEmpty) { if (_firstLoad && allMovies.isEmpty) {
return _buildSkeleton(); return _buildSkeleton();
} }
@@ -59,8 +60,8 @@ class _MovieTabPageState extends State<MovieTabPage> {
if (movies.isEmpty) { if (movies.isEmpty) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async => await provider.loadMovies(), onRefresh: () async => await provider.loadMovies(),
color: const Color(0xFF1A1A1A), color: colors.primary,
backgroundColor: Colors.white, backgroundColor: colors.surface,
child: ListView( child: ListView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
children: [_buildEmptyState(context, provider.movieStatusIndex)], children: [_buildEmptyState(context, provider.movieStatusIndex)],
@@ -77,10 +78,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
} }
Widget _buildGridView(List movies, AppProvider provider) { Widget _buildGridView(List movies, AppProvider provider) {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async => await provider.loadMovies(), onRefresh: () async => await provider.loadMovies(),
color: const Color(0xFF1A1A1A), color: colors.primary,
backgroundColor: Colors.white, backgroundColor: colors.surface,
child: GridView.builder( child: GridView.builder(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 100), padding: const EdgeInsets.fromLTRB(16, 16, 16, 100),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
@@ -96,10 +98,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
} }
Widget _buildListView(List movies, AppProvider provider) { Widget _buildListView(List movies, AppProvider provider) {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async => await provider.loadMovies(), onRefresh: () async => await provider.loadMovies(),
color: const Color(0xFF1A1A1A), color: colors.primary,
backgroundColor: Colors.white, backgroundColor: colors.surface,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100), padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
itemCount: movies.length, itemCount: movies.length,
@@ -109,6 +112,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
} }
Widget _buildListCard(movie) { Widget _buildListCard(movie) {
final colors = Theme.of(context).colorScheme;
return GestureDetector( return GestureDetector(
onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: movie), onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: movie),
onLongPress: () => _showDeleteDialog(context, movie), onLongPress: () => _showDeleteDialog(context, movie),
@@ -116,23 +120,22 @@ class _MovieTabPageState extends State<MovieTabPage> {
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFAFAFA), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Row( child: Row(
children: [ children: [
// 海报缩略图
Container( Container(
width: 48, height: 64, width: 48, height: 64,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF0F0F0), color: colors.outlineVariant,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: movie.posterPath != null && movie.posterPath!.isNotEmpty child: movie.posterPath != null && movie.posterPath!.isNotEmpty
? Image.file(File(movie.posterPath!), fit: BoxFit.cover, ? Image.file(File(movie.posterPath!), fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Icon(Icons.movie_outlined, size: 22, color: Color(0xFFCCCCCC))) errorBuilder: (_, __, ___) => Icon(Icons.movie_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)))
: const Icon(Icons.movie_outlined, size: 22, color: Color(0xFFCCCCCC)), : Icon(Icons.movie_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
@@ -140,11 +143,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(movie.title, maxLines: 1, overflow: TextOverflow.ellipsis, 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) ...[ if (movie.alternateTitles.isNotEmpty) ...[
const SizedBox(height: 3), const SizedBox(height: 3),
Text(movie.alternateTitles.take(2).join(''), maxLines: 1, overflow: TextOverflow.ellipsis, 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), const SizedBox(height: 6),
if (movie.rating != null) if (movie.rating != null)
@@ -155,7 +158,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
), ),
), ),
const SizedBox(width: 8), 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) { void _showDeleteDialog(BuildContext context, movie) {
final colors = Theme.of(context).colorScheme;
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
backgroundColor: Colors.white, backgroundColor: colors.surface,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), 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}》吗?删除后可在回收站恢复。', 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: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx), onPressed: () => Navigator.pop(ctx),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
), ),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
@@ -183,7 +187,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
Navigator.pop(ctx); Navigator.pop(ctx);
}, },
style: ElevatedButton.styleFrom( 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)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
), ),
@@ -200,6 +204,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
} }
Widget _buildListSkeleton() { Widget _buildListSkeleton() {
final colors = Theme.of(context).colorScheme;
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100), padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
itemCount: 6, itemCount: 6,
@@ -207,7 +212,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF8F8F8), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: const Row( child: const Row(
@@ -235,6 +240,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
} }
Widget _buildEmptyState(BuildContext context, int statusIndex) { Widget _buildEmptyState(BuildContext context, int statusIndex) {
final colors = Theme.of(context).colorScheme;
final statusText = ['已看', '在看', '想看'][statusIndex]; final statusText = ['已看', '在看', '想看'][statusIndex];
return Center( return Center(
child: Column( child: Column(
@@ -242,11 +248,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
children: [ children: [
Container( Container(
width: 80, height: 80, width: 80, height: 80,
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(20)), decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)),
child: const Icon(Icons.movie_outlined, size: 40, color: Color(0xFFCCCCCC)), child: Icon(Icons.movie_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
), ),
const SizedBox(height: 20), 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), const SizedBox(height: 24),
InkWell( InkWell(
onTap: () { onTap: () {
@@ -255,8 +261,8 @@ class _MovieTabPageState extends State<MovieTabPage> {
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(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

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

View File

@@ -26,21 +26,22 @@ class _NoteSharePageState extends State<NoteSharePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: colors.surfaceContainerHighest,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: colors.surface,
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)), icon: Icon(Icons.close, color: colors.onSurface),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
title: const Text( title: Text(
'分享笔记', '分享笔记',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
centerTitle: true, centerTitle: true,
@@ -53,12 +54,12 @@ class _NoteSharePageState extends State<NoteSharePage> {
height: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) )
: const Text( : Text(
'分享', '分享',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
), ),
@@ -78,6 +79,7 @@ class _NoteSharePageState extends State<NoteSharePage> {
} }
Widget _buildPosterWidget() { Widget _buildPosterWidget() {
final colors = Theme.of(context).colorScheme;
final note = widget.note; final note = widget.note;
final hasImages = note.images.isNotEmpty; final hasImages = note.images.isNotEmpty;
final dateStr = final dateStr =
@@ -88,7 +90,7 @@ class _NoteSharePageState extends State<NoteSharePage> {
return Container( return Container(
width: 320, width: 320,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -124,10 +126,10 @@ class _NoteSharePageState extends State<NoteSharePage> {
if (note.title.isNotEmpty) ...[ if (note.title.isNotEmpty) ...[
Text( Text(
note.title, note.title,
style: const TextStyle( style: TextStyle(
fontSize: 22, fontSize: 22,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@@ -136,9 +138,9 @@ class _NoteSharePageState extends State<NoteSharePage> {
// 日期 // 日期
Text( Text(
dateStr, dateStr,
style: const TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
), ),
), ),
@@ -152,12 +154,12 @@ class _NoteSharePageState extends State<NoteSharePage> {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF0F0F0), color: colors.outlineVariant,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: Text( child: Text(
tag, tag,
style: const TextStyle(fontSize: 11, color: Color(0xFF888888)), style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.5)),
), ),
); );
}).toList(), }).toList(),
@@ -165,7 +167,7 @@ class _NoteSharePageState extends State<NoteSharePage> {
], ],
const SizedBox(height: 16), const SizedBox(height: 16),
Container(height: 0.5, color: const Color(0xFFE8E8E8)), Container(height: 0.5, color: colors.outline),
const SizedBox(height: 16), const SizedBox(height: 16),
// 正文内容 // 正文内容
@@ -173,9 +175,9 @@ class _NoteSharePageState extends State<NoteSharePage> {
note.content, note.content,
maxLines: 12, maxLines: 12,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xFF333333), color: colors.onSurface.withValues(alpha: 0.75),
height: 1.8, height: 1.8,
), ),
), ),
@@ -191,11 +193,11 @@ class _NoteSharePageState extends State<NoteSharePage> {
], ],
const Spacer(), const Spacer(),
// Mooknote 品牌 // Mooknote 品牌
const Text( Text(
'Mooknote', 'Mooknote',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Color(0xFFCCCCCC), color: colors.onSurface.withValues(alpha: 0.25),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@@ -210,14 +212,15 @@ class _NoteSharePageState extends State<NoteSharePage> {
} }
Widget _buildMetaChip(IconData icon, String text) { Widget _buildMetaChip(IconData icon, String text) {
final colors = Theme.of(context).colorScheme;
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(icon, size: 13, color: const Color(0xFFAAAAAA)), Icon(icon, size: 13, color: colors.onSurface.withValues(alpha: 0.35)),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
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; bool _hasMore = true;
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
int _layoutStyle = 0; // 0: 列表, 1: 瀑布流, 2: 时间线 int _layoutStyle = 0;
bool _firstLoad = true; bool _firstLoad = true;
@override @override
@@ -110,7 +110,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
// 笔记内容
Expanded( Expanded(
child: Consumer<AppProvider>( child: Consumer<AppProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
@@ -122,10 +121,11 @@ class _NoteTabPageState extends State<NoteTabPage> {
} }
if (allNotes.isEmpty && _displayedNotes.isEmpty) { if (allNotes.isEmpty && _displayedNotes.isEmpty) {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator( return RefreshIndicator(
onRefresh: _refresh, onRefresh: _refresh,
color: const Color(0xFF1A1A1A), color: colors.primary,
backgroundColor: Colors.white, backgroundColor: colors.surface,
child: ListView( child: ListView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
children: [_buildEmptyState(context)], children: [_buildEmptyState(context)],
@@ -159,6 +159,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
} }
Widget _buildWaterfallSkeleton() { Widget _buildWaterfallSkeleton() {
final colors = Theme.of(context).colorScheme;
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100), padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
child: Row( child: Row(
@@ -168,7 +169,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
children: List.generate(4, (_) => Container( children: List.generate(4, (_) => Container(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 6, offset: const Offset(0, 2)), 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() { Widget _buildListView() {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator( return RefreshIndicator(
onRefresh: _refresh, onRefresh: _refresh,
color: const Color(0xFF1A1A1A), color: colors.primary,
backgroundColor: Colors.white, backgroundColor: colors.surface,
child: ListView.builder( child: ListView.builder(
controller: _scrollController, controller: _scrollController,
padding: const EdgeInsets.fromLTRB(12, 10, 12, 100), padding: const EdgeInsets.fromLTRB(12, 10, 12, 100),
itemCount: _displayedNotes.length + (_hasMore ? 1 : 0), itemCount: _displayedNotes.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index >= _displayedNotes.length) { if (index >= _displayedNotes.length) {
return const Padding( return Padding(
padding: EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
child: Center( child: Center(
child: SizedBox( child: SizedBox(
width: 20, height: 20, 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() { Widget _buildTimelineView() {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator( return RefreshIndicator(
onRefresh: _refresh, onRefresh: _refresh,
color: const Color(0xFF1A1A1A), color: colors.primary,
backgroundColor: Colors.white, backgroundColor: colors.surface,
child: ListView.builder( child: ListView.builder(
controller: _scrollController, controller: _scrollController,
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100), padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
itemCount: _displayedNotes.length + (_hasMore ? 1 : 0), itemCount: _displayedNotes.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index >= _displayedNotes.length) { if (index >= _displayedNotes.length) {
return const Padding( return Padding(
padding: EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
child: Center( child: Center(
child: SizedBox(width: 20, height: 20, 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) { Widget _buildTimelineItem(Note note) {
final colors = Theme.of(context).colorScheme;
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) async { Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) async {
@@ -268,72 +268,60 @@ class _NoteTabPageState extends State<NoteTabPage> {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// 左侧时间线
SizedBox( SizedBox(
width: 40, width: 40,
child: Column( child: Column(
children: [ children: [
// 圆点
Container( Container(
width: 10, width: 10,
height: 10, height: 10,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), color: colors.primary,
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2), border: Border.all(color: colors.surface, width: 2),
), ),
), ),
// 连线
Expanded( Expanded(
child: Container( child: Container(
width: 1, width: 1,
color: const Color(0xFFE5E5E5), color: colors.outline,
), ),
), ),
], ],
), ),
), ),
// 右侧内容
Expanded( Expanded(
child: Container( child: Container(
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFAFAFA), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// 时间
Text( Text(
_formatFullDate(note.updatedAt), _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) ...[ if (note.title.isNotEmpty) ...[
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
note.title, 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, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
], ],
// 内容预览
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
_getPreviewText(note), _getPreviewText(note),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, 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) ...[ if (note.tags.isNotEmpty) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Wrap( Wrap(
@@ -342,10 +330,10 @@ class _NoteTabPageState extends State<NoteTabPage> {
children: note.tags.map((tag) => Container( children: note.tags.map((tag) => Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(4), 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(), )).toList(),
), ),
], ],
@@ -364,10 +352,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
'${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; '${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
} }
// ─── 瀑布流视图 ──────────────────────────────────────────────────────
Widget _buildWaterfallView() { Widget _buildWaterfallView() {
// 分为左右两列 final colors = Theme.of(context).colorScheme;
final leftItems = <Note>[]; final leftItems = <Note>[];
final rightItems = <Note>[]; final rightItems = <Note>[];
for (int i = 0; i < _displayedNotes.length; i++) { for (int i = 0; i < _displayedNotes.length; i++) {
@@ -380,8 +366,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
return RefreshIndicator( return RefreshIndicator(
onRefresh: _refresh, onRefresh: _refresh,
color: const Color(0xFF1A1A1A), color: colors.primary,
backgroundColor: Colors.white, backgroundColor: colors.surface,
child: SingleChildScrollView( child: SingleChildScrollView(
controller: _scrollController, controller: _scrollController,
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100), padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
@@ -398,6 +384,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
} }
Widget _buildWaterfallCard(Note note) { Widget _buildWaterfallCard(Note note) {
final colors = Theme.of(context).colorScheme;
final contentText = _getPreviewText(note); final contentText = _getPreviewText(note);
final images = note.images; final images = note.images;
final hasImage = images.isNotEmpty; final hasImage = images.isNotEmpty;
@@ -413,7 +400,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
child: Container( child: Container(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -428,7 +415,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// 顶部图片
if (hasImage) if (hasImage)
Stack( Stack(
children: [ children: [
@@ -458,8 +444,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
), ),
], ],
), ),
// 底部文字区域
Padding( Padding(
padding: const EdgeInsets.fromLTRB(10, 8, 10, 10), padding: const EdgeInsets.fromLTRB(10, 8, 10, 10),
child: Column( child: Column(
@@ -471,47 +455,45 @@ class _NoteTabPageState extends State<NoteTabPage> {
note.title, note.title,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
height: 1.3, height: 1.3,
), ),
), ),
if (contentText.isNotEmpty && contentText != '(无内容)') ...[ if (contentText.isNotEmpty && contentText != '(无内容)') ...[
if (note.title.isNotEmpty) const SizedBox(height: 4), if (note.title.isNotEmpty) const SizedBox(height: 4),
Text( Text(
contentText, contentText,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Color(0xFF999999), color: colors.onSurface.withValues(alpha: 0.4),
height: 1.4, height: 1.4,
), ),
), ),
], ],
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
_formatTime(note.updatedAt), _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) if (note.tags.isNotEmpty)
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
), ),
child: Text( child: Text(
note.tags.first, 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) { void _showDeleteDialog(BuildContext context, Note note) {
final colors = Theme.of(context).colorScheme;
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: Colors.white, backgroundColor: colors.surface,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), 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: 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: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: const Color(0xFF666666), foregroundColor: colors.onSurface.withValues(alpha: 0.6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
), ),
child: const Text('取消'), child: const Text('取消'),
@@ -570,8 +553,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
Navigator.pop(context); Navigator.pop(context);
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: colors.error,
foregroundColor: Colors.white, foregroundColor: colors.onError,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@@ -584,8 +567,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
); );
} }
// ─── 数据同步 ────────────────────────────────────────────────────────
void _syncDisplayedNotes(List<Note> allNotes) { void _syncDisplayedNotes(List<Note> allNotes) {
final validNoteIds = allNotes.map((n) => n.id).toSet(); final validNoteIds = allNotes.map((n) => n.id).toSet();
final initialLength = _displayedNotes.length; final initialLength = _displayedNotes.length;
@@ -611,9 +592,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
} }
} }
// ─── 空状态 ──────────────────────────────────────────────────────────
Widget _buildEmptyState(BuildContext context) { Widget _buildEmptyState(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -621,24 +601,24 @@ class _NoteTabPageState extends State<NoteTabPage> {
Container( Container(
width: 80, height: 80, width: 80, height: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20), 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 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), const SizedBox(height: 24),
InkWell( InkWell(
onTap: () => Navigator.pushNamed(context, '/note-form'), onTap: () => Navigator.pushNamed(context, '/note-form'),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), color: colors.primary,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Text('添加记录', child: Text('添加记录',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)),
), ),
), ),
], ],

View File

@@ -51,12 +51,13 @@ class _ProfilePageState extends State<ProfilePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Column( return Column(
children: [ children: [
AppBar( AppBar(
leading: Builder( leading: Builder(
builder: (context) => IconButton( builder: (context) => IconButton(
icon: const Icon(Icons.menu, color: Color(0xFF1A1A1A)), icon: Icon(Icons.menu, color: colors.onSurface),
onPressed: () => Scaffold.of(context).openDrawer(), onPressed: () => Scaffold.of(context).openDrawer(),
), ),
), ),
@@ -64,7 +65,7 @@ class _ProfilePageState extends State<ProfilePage> {
), ),
Expanded( Expanded(
child: Container( child: Container(
color: const Color(0xFFF8F8F8), color: colors.surfaceContainerHigh,
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -98,6 +99,7 @@ class _ProfilePageState extends State<ProfilePage> {
Widget _buildUserCard() { Widget _buildUserCard() {
return Consumer<AppProvider>( return Consumer<AppProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
final colors = Theme.of(context).colorScheme;
final movieCount = provider.movies.where((m) => !m.isDeleted).length; final movieCount = provider.movies.where((m) => !m.isDeleted).length;
final bookCount = provider.books.length; final bookCount = provider.books.length;
final noteCount = provider.notes.length; final noteCount = provider.notes.length;
@@ -106,7 +108,7 @@ class _ProfilePageState extends State<ProfilePage> {
margin: const EdgeInsets.fromLTRB(16, 12, 16, 0), margin: const EdgeInsets.fromLTRB(16, 12, 16, 0),
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)), 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, width: 64, height: 64,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5), border: Border.all(color: colors.outlineVariant, width: 0.5),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: _avatarPath != null && _avatarPath!.isNotEmpty child: _avatarPath != null && _avatarPath!.isNotEmpty
? Image.file(File(_avatarPath!), fit: BoxFit.cover, ? Image.file(File(_avatarPath!), fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Icon(Icons.person_outline, size: 32, color: Color(0xFFCCCCCC))) errorBuilder: (_, __, ___) => Icon(Icons.person_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.25)))
: const Icon(Icons.person_outline, size: 32, color: Color(0xFFCCCCCC)), : Icon(Icons.person_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.25)),
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
@@ -139,25 +141,25 @@ class _ProfilePageState extends State<ProfilePage> {
children: [ children: [
GestureDetector( GestureDetector(
onTap: () => _editNickname(context), 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), const SizedBox(height: 4),
GestureDetector( GestureDetector(
onTap: () => _editMotto(context), onTap: () => _editMotto(context),
child: Text(_motto, maxLines: 1, overflow: TextOverflow.ellipsis, 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( 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), onPressed: () => _showSettings(context),
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const Divider(height: 1, color: Color(0xFFF0F0F0)), Divider(height: 1, color: colors.outlineVariant),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildStatRow(Icons.movie_outlined, _formatCount(movieCount), '观影'), _buildStatRow(Icons.movie_outlined, _formatCount(movieCount), '观影'),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -172,13 +174,14 @@ class _ProfilePageState extends State<ProfilePage> {
} }
Widget _buildStatRow(IconData icon, String count, String label) { Widget _buildStatRow(IconData icon, String count, String label) {
final colors = Theme.of(context).colorScheme;
return Row( return Row(
children: [ children: [
Icon(icon, size: 14, color: const Color(0xFFBBBBBB)), Icon(icon, size: 14, color: colors.onSurface.withValues(alpha: 0.3)),
const SizedBox(width: 8), 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(), 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() { Widget _buildExploreRow() {
final colors = Theme.of(context).colorScheme;
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)), 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( child: Row(
children: [ 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 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(), const Spacer(),
_buildQuickAction(Icons.explore_outlined, '漫步', () => Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage()))), _buildQuickAction(Icons.explore_outlined, '漫步', () => Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage()))),
const SizedBox(width: 24), const SizedBox(width: 24),
@@ -217,14 +221,15 @@ class _ProfilePageState extends State<ProfilePage> {
} }
Widget _buildQuickAction(IconData icon, String label, VoidCallback onTap) { Widget _buildQuickAction(IconData icon, String label, VoidCallback onTap) {
final colors = Theme.of(context).colorScheme;
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(icon, size: 20, color: const Color(0xFF555555)), Icon(icon, size: 20, color: colors.onSurface.withValues(alpha: 0.7)),
const SizedBox(height: 2), 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) { Widget _buildSectionHeader(String title) {
final colors = Theme.of(context).colorScheme;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 10), 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) { Widget _buildMenuGroup(List<_MenuEntry> entries) {
final colors = Theme.of(context).colorScheme;
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)), 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( Container(
width: 36, height: 36, width: 36, height: 36,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8), 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), const SizedBox(width: 12),
Expanded(child: Text(e.value.title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))), Expanded(child: Text(e.value.title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface))),
const Icon(Icons.chevron_right, size: 16, color: Color(0xFFD0D0D0)), 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(), }).toList(),
@@ -310,16 +317,17 @@ class _ProfilePageState extends State<ProfilePage> {
} }
void _editNickname(BuildContext context) { void _editNickname(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final controller = TextEditingController(text: _nickname); final controller = TextEditingController(text: _nickname);
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: Colors.white, elevation: 0, backgroundColor: colors.surface, elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
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, decoration: const InputDecoration(hintText: '输入昵称', border: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xFFE5E5E5))))), content: TextField(controller: controller, decoration: InputDecoration(hintText: '输入昵称', border: UnderlineInputBorder(borderSide: BorderSide(color: colors.outline)))),
actions: [ 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 { TextButton(onPressed: () async {
final newNickname = controller.text.trim(); final newNickname = controller.text.trim();
if (newNickname.isNotEmpty) { if (newNickname.isNotEmpty) {
@@ -334,16 +342,17 @@ class _ProfilePageState extends State<ProfilePage> {
} }
void _editMotto(BuildContext context) { void _editMotto(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final controller = TextEditingController(text: _motto); final controller = TextEditingController(text: _motto);
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: Colors.white, elevation: 0, backgroundColor: colors.surface, elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
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, maxLines: 2, decoration: const InputDecoration(hintText: '输入座右铭', border: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xFFE5E5E5))))), content: TextField(controller: controller, maxLines: 2, decoration: InputDecoration(hintText: '输入座右铭', border: UnderlineInputBorder(borderSide: BorderSide(color: colors.outline)))),
actions: [ 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 { TextButton(onPressed: () async {
final newMotto = controller.text.trim(); final newMotto = controller.text.trim();
await _userPrefs.setMotto(newMotto); await _userPrefs.setMotto(newMotto);
@@ -358,34 +367,35 @@ class _ProfilePageState extends State<ProfilePage> {
// ─── 备份弹窗 ──────────────────────────────────────────────────────── // ─── 备份弹窗 ────────────────────────────────────────────────────────
void _showBackupOptions(BuildContext context) { void _showBackupOptions(BuildContext context) {
final colors = Theme.of(context).colorScheme;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
backgroundColor: Colors.white, backgroundColor: colors.surface,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (ctx) => Padding( builder: (ctx) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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 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), const SizedBox(height: 16),
ListTile( ListTile(
contentPadding: EdgeInsets.zero, 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))), 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: const Text('本地备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), title: Text('本地备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: const Text('备份到本地文件夹,支持恢复', style: TextStyle(fontSize: 12, color: Color(0xFF999999))), subtitle: Text('备份到本地文件夹,支持恢复', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
trailing: const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC)), trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)),
onTap: () { Navigator.pop(ctx); _push(context, const BackupPage()); }, onTap: () { Navigator.pop(ctx); _push(context, const BackupPage()); },
), ),
const Divider(height: 0.5, color: Color(0xFFF0F0F0)), Divider(height: 0.5, color: colors.outlineVariant),
ListTile( ListTile(
contentPadding: EdgeInsets.zero, 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))), 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: const Text('云备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), title: Text('云备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: const Text('通过 WebDAV 同步到云端', style: TextStyle(fontSize: 12, color: Color(0xFF999999))), subtitle: Text('通过 WebDAV 同步到云端', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
trailing: const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC)), trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)),
onTap: () { Navigator.pop(ctx); _push(context, const CloudSyncPage()); }, onTap: () { Navigator.pop(ctx); _push(context, const CloudSyncPage()); },
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -421,17 +431,20 @@ class SettingsPage extends StatefulWidget {
class _SettingsPageState extends State<SettingsPage> { class _SettingsPageState extends State<SettingsPage> {
final UserPrefs _userPrefs = UserPrefs(); final UserPrefs _userPrefs = UserPrefs();
bool _hideBottomNavOnScroll = true; bool _hideBottomNavOnScroll = true;
int _themeMode = 0; // 0=系统, 1=浅色, 2=深色
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_hideBottomNavOnScroll = _userPrefs.hideBottomNavOnScroll; _hideBottomNavOnScroll = _userPrefs.hideBottomNavOnScroll;
_themeMode = _userPrefs.themeMode;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar(title: const Text('设置')), appBar: AppBar(title: const Text('设置')),
body: ListView( body: ListView(
children: [ children: [
@@ -442,7 +455,7 @@ class _SettingsPageState extends State<SettingsPage> {
subtitle: '清理未在数据库中引用的图片文件', subtitle: '清理未在数据库中引用的图片文件',
onTap: () => _showClearCacheDialog(context), onTap: () => _showClearCacheDialog(context),
), ),
const Divider(height: 0.5, indent: 24, endIndent: 24), Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildSwitchItem( _buildSwitchItem(
icon: Icons.swipe_vertical_outlined, icon: Icons.swipe_vertical_outlined,
title: '底部导航栏滚动隐藏', title: '底部导航栏滚动隐藏',
@@ -450,28 +463,31 @@ class _SettingsPageState extends State<SettingsPage> {
value: _hideBottomNavOnScroll, value: _hideBottomNavOnScroll,
onChanged: _toggleHideBottomNavOnScroll, onChanged: _toggleHideBottomNavOnScroll,
), ),
const Divider(height: 0.5, indent: 24, endIndent: 24), Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildNavigationItem( _buildNavigationItem(
icon: Icons.apps_outlined, icon: Icons.apps_outlined,
title: '应用图标', title: '应用图标',
subtitle: '更换桌面应用图标', subtitle: '更换桌面应用图标',
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AppIconPickerPage())), 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( _buildNavigationItem(
icon: Icons.view_list_outlined, icon: Icons.view_list_outlined,
title: '主界面设置', title: '主界面设置',
subtitle: '启动标签、模块显示开关', subtitle: '启动标签、模块显示开关',
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MainContentSettingsPage())), 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( _buildNavigationItem(
icon: Icons.dashboard_outlined, icon: Icons.dashboard_outlined,
title: '布局设置', title: '布局设置',
subtitle: '笔记、影视、阅读的展示样式', subtitle: '笔记、影视、阅读的展示样式',
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const LayoutSettingsPage())), 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('帮助'), _buildSectionHeader('帮助'),
_buildLinkItem( _buildLinkItem(
context: context, context: context,
@@ -480,7 +496,7 @@ class _SettingsPageState extends State<SettingsPage> {
subtitle: '查看应用使用指南', subtitle: '查看应用使用指南',
url: 'https://mooknote.iletter.top/#/guide', 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); 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}) { 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( return InkWell(
onTap: () => onChanged(!value), onTap: () => onChanged(!value),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row( child: Row(
children: [ 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), const SizedBox(width: 16),
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 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), 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) { Widget _buildSectionHeader(String title) {
final colors = Theme.of(context).colorScheme;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(24, 32, 24, 12), 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}) { Widget _buildNavigationItem({required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
final colors = Theme.of(context).colorScheme;
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row( child: Row(
children: [ 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), const SizedBox(width: 16),
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 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), 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}) { 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( return InkWell(
onTap: () => _launchUrl(context, url), onTap: () => _launchUrl(context, url),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row( child: Row(
children: [ 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), const SizedBox(width: 16),
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 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), 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}) { Widget _buildActionItem({required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
final colors = Theme.of(context).colorScheme;
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row( child: Row(
children: [ 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), const SizedBox(width: 16),
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 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), 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) { void _showClearCacheDialog(BuildContext pageContext) {
final colors = Theme.of(context).colorScheme;
showDialog( showDialog(
context: pageContext, context: pageContext,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AlertDialog(
backgroundColor: Colors.white, elevation: 0, backgroundColor: colors.surface, elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('清除缓存数据'), title: const Text('清除缓存数据'),
content: const Text('这将删除所有未在数据库中引用的图片文件。确定要继续吗?'), content: const Text('这将删除所有未在数据库中引用的图片文件。确定要继续吗?'),
actions: [ 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 { TextButton(onPressed: () async {
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
await _clearCacheData(pageContext); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar(title: const Text('主界面设置')), appBar: AppBar(title: const Text('主界面设置')),
body: ListView( body: ListView(
children: [ children: [
_buildSectionHeader('启动设置'), _buildSectionHeader('启动设置'),
_buildDefaultTabSelector(), _buildDefaultTabSelector(),
const Divider(height: 0.5, indent: 24, endIndent: 24), Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildSectionHeader('模块开关'), _buildSectionHeader('模块开关'),
_buildSwitchItem(Icons.movie_outlined, '观影', '记录和管理观影记录', _showMovieTab, _toggleMovieTab), _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), _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), _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( Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), 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) { Widget _buildSectionHeader(String title) {
final colors = Theme.of(context).colorScheme;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12), 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) { Widget _buildSwitchItem(IconData icon, String title, String subtitle, bool value, ValueChanged<bool> onChanged) {
final colors = Theme.of(context).colorScheme;
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), 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)), 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: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), title: Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
trailing: Switch(value: value, onChanged: onChanged, activeColor: const Color(0xFF1A1A1A), activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), inactiveThumbColor: Colors.white, inactiveTrackColor: const Color(0xFFE5E5E5)), trailing: Switch(value: value, onChanged: onChanged, activeColor: colors.primary, activeTrackColor: colors.primary.withOpacity(0.3), inactiveThumbColor: colors.surface, inactiveTrackColor: colors.outline),
); );
} }
Widget _buildDefaultTabSelector() { Widget _buildDefaultTabSelector() {
final colors = Theme.of(context).colorScheme;
final labels = ['影视', '阅读', '笔记']; final labels = ['影视', '阅读', '笔记'];
final icons = [Icons.movie_outlined, Icons.menu_book_outlined, Icons.note_outlined]; final icons = [Icons.movie_outlined, Icons.menu_book_outlined, Icons.note_outlined];
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), 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)), 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: const Text('默认启动标签', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), title: Text('默认启动标签', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: const Text('打开应用时默认显示的页面', style: TextStyle(fontSize: 12, color: Color(0xFF999999))), subtitle: Text('打开应用时默认显示的页面', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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 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), onTap: () => _showDefaultTabPicker(labels, icons),
@@ -775,24 +870,25 @@ class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
} }
void _showDefaultTabPicker(List<String> labels, List<IconData> icons) { void _showDefaultTabPicker(List<String> labels, List<IconData> icons) {
final colors = Theme.of(context).colorScheme;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
builder: (ctx) => Container( 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), padding: const EdgeInsets.symmetric(vertical: 12),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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 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), const SizedBox(height: 16),
for (int i = 0; i < labels.length; i++) for (int i = 0; i < labels.length; i++)
ListTile( 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))), 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: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), title: Text(labels[i], style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
trailing: _defaultTabIndex == i ? const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 20) : null, trailing: _defaultTabIndex == i ? Icon(Icons.check, color: colors.onSurface, size: 20) : null,
onTap: () async { await _userPrefs.setDefaultMainTabIndex(i); setState(() => _defaultTabIndex = i); Navigator.pop(ctx); }, onTap: () async { await _userPrefs.setDefaultMainTabIndex(i); setState(() => _defaultTabIndex = i); Navigator.pop(ctx); },
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -828,28 +924,29 @@ class _LayoutSettingsPageState extends State<LayoutSettingsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar(title: const Text('布局设置')), appBar: AppBar(title: const Text('布局设置')),
body: ListView( body: ListView(
children: [ children: [
_buildSectionHeader('笔记布局'), _buildSectionHeader('笔记布局'),
_buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列列表,简洁清晰', value: 0, groupValue: _noteLayout, onTap: () => _setLayout('note', 0)), _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)), _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)), _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('影视布局'), _buildSectionHeader('影视布局'),
_buildLayoutOption(icon: Icons.grid_view_outlined, title: '海报网格', subtitle: '三列海报,赏心悦目', value: 0, groupValue: _movieLayout, onTap: () => _setLayout('movie', 0)), _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)), _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('阅读布局'), _buildSectionHeader('阅读布局'),
_buildLayoutOption(icon: Icons.grid_view_outlined, title: '封面网格', subtitle: '三列封面,清新雅致', value: 0, groupValue: _bookLayout, onTap: () => _setLayout('book', 0)), _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)), _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) { Widget _buildSectionHeader(String title) {
final colors = Theme.of(context).colorScheme;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12), 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}) { 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; final selected = value == groupValue;
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), 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)), 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: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), title: Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
trailing: selected ? const Icon(Icons.check_circle, color: Color(0xFF1A1A1A), size: 20) : const Icon(Icons.circle_outlined, color: Color(0xFFDDDDDD), size: 20), 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, onTap: onTap,
); );
} }
@@ -911,10 +1010,11 @@ class _WebViewPageState extends State<WebViewPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar(title: const Text(''), actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: () => _controller.reload())]), 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> { class _RecycleBinPageState extends State<RecycleBinPage> {
List<_DeletedItem> _allItems = []; List<_DeletedItem> _allItems = [];
_ItemType? _filterType; // null = 全部 _ItemType? _filterType;
bool _isLoading = true; bool _isLoading = true;
List<_DeletedItem> get _filteredItems => List<_DeletedItem> get _filteredItems =>
@@ -80,8 +80,9 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar( appBar: AppBar(
title: const Text('回收站'), title: const Text('回收站'),
actions: [ actions: [
@@ -91,12 +92,12 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
child: TextButton( child: TextButton(
onPressed: _showClearAllDialog, onPressed: _showClearAllDialog,
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: Colors.white, backgroundColor: colors.surface,
foregroundColor: Colors.red, foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), 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, minimumSize: Size.zero,
), ),
@@ -106,12 +107,10 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
], ],
), ),
body: _isLoading body: _isLoading
? const Center(child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A))) ? Center(child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary))
: Column( : Column(
children: [ children: [
// 筛选标签行
if (_allItems.isNotEmpty) _buildFilterRow(), if (_allItems.isNotEmpty) _buildFilterRow(),
// 列表
Expanded( Expanded(
child: _filteredItems.isEmpty child: _filteredItems.isEmpty
? _buildEmptyState() ? _buildEmptyState()
@@ -130,11 +129,12 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
} }
Widget _buildFilterRow() { Widget _buildFilterRow() {
final colors = Theme.of(context).colorScheme;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: const BoxDecoration( decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Color(0xFFEEEEEE), width: 0.5)), border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5)),
), ),
child: Wrap( child: Wrap(
spacing: 8, spacing: 8,
@@ -149,6 +149,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
} }
Widget _filterChip(String label, _ItemType? type) { Widget _filterChip(String label, _ItemType? type) {
final colors = Theme.of(context).colorScheme;
final active = _filterType == type; final active = _filterType == type;
final count = type == null ? _allItems.length : _allItems.where((i) => i.type == type).length; final count = type == null ? _allItems.length : _allItems.where((i) => i.type == type).length;
return GestureDetector( return GestureDetector(
@@ -156,7 +157,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: active ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5), color: active ? colors.primary : colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Text( child: Text(
@@ -164,7 +165,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, 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) { Widget _buildCard(_DeletedItem item) {
final colors = Theme.of(context).colorScheme;
return Dismissible( return Dismissible(
key: Key('${item.type.name}_${item.id}'), key: Key('${item.type.name}_${item.id}'),
direction: DismissDirection.endToStart, direction: DismissDirection.endToStart,
@@ -181,9 +183,9 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
child: Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFAFAFA), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8), 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( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
@@ -193,11 +195,11 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
width: 40, width: 40,
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(8), 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), const SizedBox(width: 12),
Expanded( Expanded(
@@ -210,7 +212,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
Expanded( Expanded(
child: Text( child: Text(
item.title, 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, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -219,13 +221,13 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(3), 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( child: Text(
item.typeLabel, 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), const SizedBox(height: 3),
Text( Text(
item.subtitle, 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), const SizedBox(width: 8),
_actionBtn(Icons.restore, '恢复', const Color(0xFF1A1A1A), () => _restore(item)), _actionBtn(Icons.restore, '恢复', colors.primary, () => _restore(item)),
const SizedBox(width: 6), 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) { Widget _actionBtn(IconData icon, String tooltip, Color color, VoidCallback onTap) {
final colors = Theme.of(context).colorScheme;
return Material( return Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
@@ -274,9 +277,9 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
width: 32, width: 32,
height: 32, height: 32,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(6), 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), child: Icon(icon, size: 16, color: color),
), ),
@@ -286,6 +289,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
} }
Widget _buildEmptyState() { Widget _buildEmptyState() {
final colors = Theme.of(context).colorScheme;
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -294,18 +298,18 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
width: 72, width: 72,
height: 72, height: 72,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF8F8F8), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(18), 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), const SizedBox(height: 16),
Text( Text(
_filterType == null ? '回收站是空的' : '没有删除的项目', _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 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 { Future<bool> _showConfirmDialog(String message) async {
final colors = Theme.of(context).colorScheme;
final result = await showDialog<bool>( final result = await showDialog<bool>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
backgroundColor: Colors.white, backgroundColor: colors.surface,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), 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(message, style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)), content: Text(message, style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx, false), 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('取消'), child: const Text('取消'),
), ),
ElevatedButton( ElevatedButton(
onPressed: () => Navigator.pop(ctx, true), onPressed: () => Navigator.pop(ctx, true),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: colors.error,
foregroundColor: Colors.white, foregroundColor: colors.onError,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
), ),
@@ -377,27 +382,28 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
void _showClearAllDialog() { void _showClearAllDialog() {
final pageContext = context; final pageContext = context;
final colors = Theme.of(pageContext).colorScheme;
showDialog( showDialog(
context: pageContext, context: pageContext,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
backgroundColor: Colors.white, backgroundColor: colors.surface,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Row( title: Row(
children: [ children: [
Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22), const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
SizedBox(width: 8), const SizedBox(width: 8),
Text('清空回收站', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), 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: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx), onPressed: () => Navigator.pop(ctx),
style: TextButton.styleFrom(foregroundColor: const Color(0xFF666666)), style: TextButton.styleFrom(foregroundColor: colors.onSurface.withValues(alpha: 0.6)),
child: const Text('取消'), child: const Text('取消'),
), ),
ElevatedButton( ElevatedButton(
@@ -408,8 +414,8 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
if (mounted) ToastUtil.show(pageContext, '回收站已清空'); if (mounted) ToastUtil.show(pageContext, '回收站已清空');
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: colors.error,
foregroundColor: Colors.white, foregroundColor: colors.onError,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
), ),

View File

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

View File

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

View File

@@ -153,8 +153,9 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar( appBar: AppBar(
title: const Text('漫步'), title: const Text('漫步'),
actions: [ actions: [
@@ -166,15 +167,15 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), color: colors.primary,
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(18),
), ),
child: const Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.casino_outlined, size: 14, color: Colors.white), Icon(Icons.casino_outlined, size: 14, color: colors.onPrimary),
SizedBox(width: 5), const SizedBox(width: 5),
Text('随机', style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.w500)), 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 body: _currentItem == null
? const Center(child: Text('还没有任何内容\n去添加一些吧', textAlign: TextAlign.center, ? Center(
style: TextStyle(fontSize: 15, color: Color(0xFFBBBBBB), height: 1.6))) 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, _) { : Consumer<AppProvider>(builder: (context, provider, _) {
final item = _currentItem!; final item = _currentItem!;
final hasImage = item.imagePath != null && item.imagePath!.isNotEmpty && File(item.imagePath!).existsSync(); 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( child: Text(
item.title, item.title,
textAlign: TextAlign.center, 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(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(item.subtitle, textAlign: TextAlign.center, 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), margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFAFAFA), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text(item.detail, 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), 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), const SizedBox(height: 40),
], ],
@@ -302,11 +305,12 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
} }
Widget _buildNoteCard(_StrollItem item, bool hasImage) { Widget _buildNoteCard(_StrollItem item, bool hasImage) {
final colors = Theme.of(context).colorScheme;
return Container( return Container(
width: double.infinity, width: double.infinity,
constraints: const BoxConstraints(maxWidth: 360), constraints: const BoxConstraints(maxWidth: 360),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.06), blurRadius: 16, offset: const Offset(0, 6)), 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(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Text(item.detail.isEmpty ? '(无内容)' : item.detail, 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(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), 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) { Widget _buildPlaceholder(_StrollItem item) {
final colors = Theme.of(context).colorScheme;
return Container( return Container(
color: const Color(0xFFF8F8F8), color: colors.surfaceContainerHigh,
child: Center( child: Center(
child: Icon(item.icon, size: 48, color: item.color.withValues(alpha: 0.2)), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar( appBar: AppBar(
title: const Text('本地备份'), title: const Text('本地备份'),
), ),
body: _isLoading body: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: ListView( : ListView(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
children: [ children: [
// 自动备份开关 - 紧凑一行 // 自动备份开关 - 紧凑一行
_buildAutoBackupSection(), _buildAutoBackupSection(colors),
const SizedBox(height: 24), const SizedBox(height: 24),
// 手动备份 // 手动备份
_buildSectionTitle('手动备份'), _buildSectionTitle(colors, '手动备份'),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildActionCard( _buildActionCard(
title: '导出数据', colors: colors,
description: '将所有数据导出为 zip 文件,可用于备份或迁移到其他设备', title: '导出数据',
icon: Icons.upload_outlined, description: '将所有数据导出为 zip 文件,可用于备份或迁移到其他设备',
buttonText: '导出', icon: Icons.upload_outlined,
isLoading: _isExporting, buttonText: '导出',
onTap: _exportData, isLoading: _isExporting,
), onTap: _exportData,
const SizedBox(height: 12), ),
_buildActionCard( const SizedBox(height: 12),
title: '导入数据', _buildActionCard(
description: '从备份文件导入数据,将覆盖当前所有数据', colors: colors,
icon: Icons.download_outlined, title: '导入数据',
buttonText: '导入', description: '从备份文件导入数据,将覆盖当前所有数据',
isLoading: _isImporting, icon: Icons.download_outlined,
onTap: _importData, buttonText: '导入',
isDestructive: true, 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( return Row(
children: [ children: [
Container( Container(
width: 4, width: 4,
height: 16, height: 16,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), color: colors.primary,
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
title, title,
style: const TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
], ],
@@ -113,6 +116,7 @@ class _BackupPageState extends State<BackupPage> {
/// 构建操作卡片 /// 构建操作卡片
Widget _buildActionCard({ Widget _buildActionCard({
required ColorScheme colors,
required String title, required String title,
required String description, required String description,
required IconData icon, required IconData icon,
@@ -124,7 +128,7 @@ class _BackupPageState extends State<BackupPage> {
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFAFAFA), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(
@@ -136,19 +140,17 @@ class _BackupPageState extends State<BackupPage> {
width: 44, width: 44,
height: 44, height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all( border: Border.all(
color: isDestructive color: isDestructive ? Colors.red.withOpacity(0.3) : colors.outline,
? Colors.red.withOpacity(0.3)
: const Color(0xFFE8E8E8),
width: 0.5, width: 0.5,
), ),
), ),
child: Icon( child: Icon(
icon, icon,
size: 22, size: 22,
color: isDestructive ? Colors.red : const Color(0xFF666666), color: isDestructive ? Colors.red : colors.onSurface.withValues(alpha: 0.6),
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
@@ -158,18 +160,18 @@ class _BackupPageState extends State<BackupPage> {
children: [ children: [
Text( Text(
title, title,
style: const TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
description, description,
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Color(0xFF666666), color: colors.onSurface.withValues(alpha: 0.6),
height: 1.4, height: 1.4,
), ),
), ),
@@ -185,25 +187,25 @@ class _BackupPageState extends State<BackupPage> {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isLoading ? const Color(0xFFCCCCCC) : const Color(0xFF1A1A1A), color: isLoading ? colors.onSurface.withValues(alpha: 0.25) : colors.primary,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Center( child: Center(
child: isLoading child: isLoading
? const SizedBox( ? SizedBox(
width: 20, width: 20,
height: 20, height: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(Colors.white), valueColor: AlwaysStoppedAnimation(colors.onPrimary),
), ),
) )
: Text( : Text(
buttonText, buttonText,
style: const TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w500, 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( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF8F8F8), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(
@@ -231,42 +233,42 @@ class _BackupPageState extends State<BackupPage> {
width: 32, width: 32,
height: 32, height: 32,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(8), 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, Icons.info_outline,
size: 18, size: 18,
color: Color(0xFF666666), color: colors.onSurface.withValues(alpha: 0.6),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
const Text( Text(
'使用说明', '使用说明',
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A), color: colors.onSurface,
), ),
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildInfoItem('1', '导出数据会生成一个 .zip 文件,包含所有数据和图片'), _buildInfoItem(colors, '1', '导出数据会生成一个 .zip 文件,包含所有数据和图片'),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildInfoItem('2', '选择保存路径后,可以通过微信、邮件等方式发送备份文件'), _buildInfoItem(colors, '2', '选择保存路径后,可以通过微信、邮件等方式发送备份文件'),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildInfoItem('3', '在新设备上选择导入数据,选择备份文件即可恢复'), _buildInfoItem(colors, '3', '在新设备上选择导入数据,选择备份文件即可恢复'),
const SizedBox(height: 12), 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( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -274,16 +276,16 @@ class _BackupPageState extends State<BackupPage> {
width: 20, width: 20,
height: 20, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFE8E8E8), color: colors.outline,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Center( child: Center(
child: Text( child: Text(
number, number,
style: const TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF666666), color: colors.onSurface.withValues(alpha: 0.6),
), ),
), ),
), ),
@@ -292,9 +294,9 @@ class _BackupPageState extends State<BackupPage> {
Expanded( Expanded(
child: Text( child: Text(
text, text,
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Color(0xFF666666), color: colors.onSurface.withValues(alpha: 0.6),
height: 1.5, height: 1.5,
), ),
), ),
@@ -311,85 +313,93 @@ class _BackupPageState extends State<BackupPage> {
}) { }) {
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) {
backgroundColor: Colors.white, final colors = Theme.of(ctx).colorScheme;
elevation: 0, return AlertDialog(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), backgroundColor: colors.surface,
title: Column( elevation: 0,
children: [ shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
Container( title: Column(
width: 48, children: [
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),
Container( Container(
width: double.infinity, width: 48,
padding: const EdgeInsets.all(12), height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF8F8F8), color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(12),
),
child: Text(
detail,
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
maxLines: 3,
overflow: TextOverflow.ellipsis,
), ),
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 { Future<void> _exportData() async {
setState(() => _isExporting = true); setState(() => _isExporting = true);
try { try {
final result = await BackupService.instance.exportDataWithImages(); final result = await BackupService.instance.exportDataWithImages();
if (!mounted) return; if (!mounted) return;
if (result.cancelled) { if (result.cancelled) {
ToastUtil.show(context, '已取消导出'); ToastUtil.show(context, '已取消导出');
} else if (result.success) { } else if (result.success) {
_showSuccessDialog( _showSuccessDialog(
title: '导出成功', title: '导出成功',
content: '备份文件已保存,包含:\n影视 ${result.movieCount} · 书籍 ${result.bookCount} · 笔记 ${result.noteCount} · 图片 ${result.imageCount}', content:
'备份文件已保存,包含:\n影视 ${result.movieCount} · 书籍 ${result.bookCount} · 笔记 ${result.noteCount} · 图片 ${result.imageCount}',
detail: result.filePath ?? '', detail: result.filePath ?? '',
); );
} else { } else {
@@ -411,54 +421,63 @@ class _BackupPageState extends State<BackupPage> {
// 显示确认对话框 // 显示确认对话框
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (ctx) {
backgroundColor: Colors.white, final colors = Theme.of(ctx).colorScheme;
elevation: 0, return AlertDialog(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), backgroundColor: colors.surface,
title: Row( elevation: 0,
children: [ shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
Container( title: Row(
width: 40, children: [
height: 40, Container(
decoration: BoxDecoration( width: 40,
color: Colors.red.withOpacity(0.08), height: 40,
borderRadius: BorderRadius.circular(10), 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; if (confirmed != true) return;
@@ -467,7 +486,7 @@ class _BackupPageState extends State<BackupPage> {
try { try {
final result = await BackupService.instance.importData(); final result = await BackupService.instance.importData();
if (!mounted) return; if (!mounted) return;
if (result.cancelled) { if (result.cancelled) {
@@ -499,13 +518,13 @@ class _BackupPageState extends State<BackupPage> {
} }
/// 构建自动备份区域 - 紧凑一行 /// 构建自动备份区域 - 紧凑一行
Widget _buildAutoBackupSection() { Widget _buildAutoBackupSection(ColorScheme colors) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFAFAFA), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12), 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( child: Row(
children: [ children: [
@@ -513,11 +532,12 @@ class _BackupPageState extends State<BackupPage> {
width: 40, width: 40,
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(10), 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), const SizedBox(width: 12),
Expanded( Expanded(
@@ -525,22 +545,25 @@ class _BackupPageState extends State<BackupPage> {
? Column( ? Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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), const SizedBox(height: 2),
Text( Text(
_backupDirPath!, _backupDirPath!,
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)), style:
TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, 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( Switch(
@@ -555,14 +578,13 @@ class _BackupPageState extends State<BackupPage> {
} }
await _loadAutoBackupStatus(); await _loadAutoBackupStatus();
}, },
activeColor: const Color(0xFF1A1A1A), activeThumbColor: colors.primary,
activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), activeTrackColor: colors.primary.withValues(alpha: 0.3),
inactiveThumbColor: Colors.white, inactiveThumbColor: colors.surface,
inactiveTrackColor: const Color(0xFFE5E5E5), inactiveTrackColor: colors.outline,
), ),
], ],
), ),
); );
} }
} }

View File

@@ -8,95 +8,163 @@ class CloudSyncPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F8F8), backgroundColor: colors.surfaceContainerHigh,
appBar: AppBar(title: const Text('云备份')), appBar: AppBar(title: const Text('云备份')),
body: ListView( body: ListView(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
children: [ children: [
_buildSectionTitle('选择备份方式'), _buildSectionTitle(colors, '选择备份方式'),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildOption( _buildOption(
colors: colors,
icon: Icons.storage_outlined, icon: Icons.storage_outlined,
title: 'WebDAV 备份', title: 'WebDAV 备份',
subtitle: '通过 WebDAV 协议备份到个人云盘', subtitle: '通过 WebDAV 协议备份到个人云盘',
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())), onTap: () =>
Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildOption( _buildOption(
colors: colors,
icon: Icons.sync_outlined, icon: Icons.sync_outlined,
title: '服务端实时同步', title: '服务端实时同步',
subtitle: '自建服务端,多设备数据实时同步', subtitle: '自建服务端,多设备数据实时同步',
enabled: false, enabled: false,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ServerSyncPage())), onTap: () =>
Navigator.push(context, MaterialPageRoute(builder: (_) => const ServerSyncPage())),
), ),
const SizedBox(height: 28), const SizedBox(height: 28),
_buildInfo(), _buildInfo(colors),
], ],
), ),
); );
} }
Widget _buildSectionTitle(String title) { Widget _buildSectionTitle(ColorScheme colors, String title) {
return Row(children: [ 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), 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( return GestureDetector(
onTap: enabled ? onTap : null, onTap: enabled ? onTap : null,
child: Container( child: Container(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration( decoration: BoxDecoration(
color: enabled ? Colors.white : const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(14), color: enabled ? colors.surface : colors.surfaceContainerHighest,
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))], borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.03),
blurRadius: 6,
offset: const Offset(0, 2)),
],
), ),
child: Row(children: [ 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), const SizedBox(width: 14),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: enabled ? const Color(0xFF1A1A1A) : const Color(0xFFBBBBBB))), 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), 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( return Container(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14), decoration: BoxDecoration(
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))], 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: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(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 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), const SizedBox(height: 14),
_infoItem('WebDAV 备份:将数据备份到支持 WebDAV 的云盘'), _infoItem(colors, 'WebDAV 备份:将数据备份到支持 WebDAV 的云盘'),
const SizedBox(height: 8), const SizedBox(height: 8),
_infoItem('服务端实时同步:通过自建服务端实现多设备实时同步'), _infoItem(colors, '服务端实时同步:通过自建服务端实现多设备实时同步'),
const SizedBox(height: 8), const SizedBox(height: 8),
_infoItem('激活码由服务端管理员在管理后台生成'), _infoItem(colors, '激活码由服务端管理员在管理后台生成'),
const SizedBox(height: 8), const SizedBox(height: 8),
_infoItem('建议定期备份 + 实时同步配合使用'), _infoItem(colors, '建议定期备份 + 实时同步配合使用'),
]), ]),
); );
} }
Widget _infoItem(String text) { Widget _infoItem(ColorScheme colors, String text) {
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ 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), 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 { Future<void> _disconnect() async {
final confirm = await showDialog<bool>( final confirm = await showDialog<bool>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) {
backgroundColor: Colors.white, final colors = Theme.of(ctx).colorScheme;
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), return AlertDialog(
title: const Text('断开连接', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), backgroundColor: colors.surface,
content: const Text('将清除服务器配置和激活信息,确定要断开吗?', style: TextStyle(fontSize: 14, color: Color(0xFF666666))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
actions: [ title: const Text('断开连接', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))), content: Text('将清除服务器配置和激活信息,确定要断开吗?',
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定', style: TextStyle(color: Color(0xFFE53935)))), 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; if (confirm != true) return;
@@ -190,63 +198,107 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F8F8), backgroundColor: colors.surfaceContainerHigh,
appBar: AppBar(title: const Text('服务端实时同步')), appBar: AppBar(title: const Text('服务端实时同步')),
body: ListView(padding: const EdgeInsets.all(20), children: [ body: ListView(padding: const EdgeInsets.all(20), children: [
// 状态卡片 // 状态卡片
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), decoration: BoxDecoration(
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))], 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: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
Container(width: 10, height: 10, decoration: BoxDecoration( Container(
color: _isActivated ? const Color(0xFF66BB6A) : const Color(0xFFDDDDDD), shape: BoxShape.circle)), width: 10,
height: 10,
decoration: BoxDecoration(
color: _isActivated
? const Color(0xFF66BB6A)
: colors.onSurface.withValues(alpha: 0.15),
shape: BoxShape.circle)),
const SizedBox(width: 10), const SizedBox(width: 10),
Text(_isActivated ? '已激活' : '未激活', Text(_isActivated ? '已激活' : '未激活',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, style: TextStyle(
color: _isActivated ? const Color(0xFF66BB6A) : const Color(0xFFBBBBBB))), fontSize: 16,
fontWeight: FontWeight.w600,
color: _isActivated
? const Color(0xFF66BB6A)
: colors.onSurface.withValues(alpha: 0.3))),
const Spacer(), const Spacer(),
if (_isActivated) if (_isActivated)
GestureDetector( GestureDetector(
onTap: _disconnect, onTap: _disconnect,
child: Container(padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), child: Container(
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(6)), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
child: const Text('断开', style: TextStyle(fontSize: 12, color: Color(0xFFE57373)))), decoration: BoxDecoration(
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(6)),
child: const Text('断开',
style: TextStyle(fontSize: 12, color: Color(0xFFE57373)))),
), ),
]), ]),
const SizedBox(height: 20), 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), const SizedBox(height: 6),
TextField(controller: _urlController, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)), TextField(
decoration: _inputDeco('例: http://192.168.1.100:5000')), controller: _urlController,
style: TextStyle(fontSize: 14, color: colors.onSurface),
decoration: _inputDeco(colors, '例: http://192.168.1.100:5000'),
),
const SizedBox(height: 14), 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), const SizedBox(height: 6),
TextField(controller: _codeController, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)), TextField(
textCapitalization: TextCapitalization.characters, decoration: _inputDeco('例: MK-A1B2C3D4E5F6')), controller: _codeController,
style: TextStyle(fontSize: 14, color: colors.onSurface),
textCapitalization: TextCapitalization.characters,
decoration: _inputDeco(colors, '例: MK-A1B2C3D4E5F6')),
const SizedBox(height: 16), const SizedBox(height: 16),
SizedBox(width: double.infinity, SizedBox(
width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: _isChecking ? null : _checkActivation, onPressed: _isChecking ? null : _checkActivation,
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF1A1A1A), foregroundColor: Colors.white, style: ElevatedButton.styleFrom(
disabledBackgroundColor: const Color(0xFFDDDDDD), elevation: 0, backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
disabledBackgroundColor: colors.onSurface.withValues(alpha: 0.15),
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
padding: const EdgeInsets.symmetric(vertical: 13)), padding: const EdgeInsets.symmetric(vertical: 13)),
child: _isChecking child: _isChecking
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) ? SizedBox(
: Text(_isActivated ? '重新验证' : '验证激活', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: colors.onPrimary))
: Text(_isActivated ? '重新验证' : '验证激活',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
), ),
), ),
if (_expiresText.isNotEmpty) ...[ if (_expiresText.isNotEmpty) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
Center(child: Row(mainAxisSize: MainAxisSize.min, children: [ Center(
Icon(Icons.access_time, size: 14, color: _prefs.syncIsPermanent ? const Color(0xFF66BB6A) : const Color(0xFFFF9800)), 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), const SizedBox(width: 6),
Text(_expiresText, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, Text(_expiresText,
color: _prefs.syncIsPermanent ? const Color(0xFF66BB6A) : const Color(0xFFFF9800))), 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( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), decoration: BoxDecoration(
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))]), 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: [ child: Row(children: [
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), Container(
child: Icon(_syncEnabled ? Icons.sync : Icons.sync_disabled, width: 44,
color: _syncEnabled ? const Color(0xFF1A1A1A) : const Color(0xFFCCCCCC), size: 22)), 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), const SizedBox(width: 14),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(
const Text('服务端实时同步', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('服务端实时同步',
style: TextStyle(
fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
const SizedBox(height: 2), const SizedBox(height: 2),
Text(_syncEnabled ? '使用服务端数据,多设备实时共享' : '关闭后下载数据到本地使用', 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, Switch(
activeColor: const Color(0xFF1A1A1A), activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), value: _syncEnabled,
inactiveThumbColor: Colors.white, inactiveTrackColor: const Color(0xFFE5E5E5)), onChanged: _isActivated ? _toggleSync : null,
activeThumbColor: colors.primary,
activeTrackColor: colors.primary.withValues(alpha: 0.3),
inactiveThumbColor: colors.surface,
inactiveTrackColor: colors.outline),
]), ]),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// 说明 // 说明
Container( Container(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14), decoration: BoxDecoration(
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))]), 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: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
Container(width: 36, height: 36, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(8)), Container(
child: const Icon(Icons.info_outline, size: 18, color: Color(0xFF666666))), 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 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), const SizedBox(height: 14),
_infoItem('1. 在服务端管理后台生成激活码'), _infoItem(colors, '1. 在服务端管理后台生成激活码'),
const SizedBox(height: 8), const SizedBox(height: 8),
_infoItem('2. 输入服务器地址和激活码完成验证'), _infoItem(colors, '2. 输入服务器地址和激活码完成验证'),
const SizedBox(height: 8), const SizedBox(height: 8),
_infoItem('3. 验证通过后自动开启实时同步'), _infoItem(colors, '3. 验证通过后自动开启实时同步'),
const SizedBox(height: 8), const SizedBox(height: 8),
_infoItem('4. 开启时所有数据通过服务端接口操作'), _infoItem(colors, '4. 开启时所有数据通过服务端接口操作'),
const SizedBox(height: 8), const SizedBox(height: 8),
_infoItem('5. 关闭时从服务端下载数据到本地使用'), _infoItem(colors, '5. 关闭时从服务端下载数据到本地使用'),
]), ]),
), ),
const SizedBox(height: 40), 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( return InputDecoration(
hintText: hint, hintStyle: const TextStyle(fontSize: 13, color: Color(0xFFCCCCCC)), hintText: hint,
filled: true, fillColor: const Color(0xFFF8F8F8), hintStyle: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.25)),
filled: true,
fillColor: colors.surfaceContainerHigh,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), 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: [ 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), 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 password = _passwordController.text;
final path = _pathController.text.trim(); final path = _pathController.text.trim();
if (url.isEmpty) { ToastUtil.show(context, '请输入服务器地址'); return; } if (url.isEmpty) {
if (username.isEmpty) { ToastUtil.show(context, '请输入用户名'); return; } ToastUtil.show(context, '请输入服务器地址');
if (password.isEmpty) { ToastUtil.show(context, '请输入密码'); return; } return;
}
if (username.isEmpty) {
ToastUtil.show(context, '请输入用户名');
return;
}
if (password.isEmpty) {
ToastUtil.show(context, '请输入密码');
return;
}
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
final result = await WebDAVService.instance.testConnection( 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 (!mounted) return;
if (result['success'] == true) { if (result['success'] == true) {
await WebDAVService.instance.saveConfig( await WebDAVService.instance.saveConfig(
url: url, username: username, password: password, path: path, url: url,
username: username,
password: password,
path: path,
); );
setState(() => _isConfigured = true); setState(() => _isConfigured = true);
ToastUtil.show(context, result['message'] ?? '连接成功,配置已保存'); ToastUtil.show(context, result['message'] ?? '连接成功,配置已保存');
@@ -104,7 +119,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
if (result.success) { if (result.success) {
final details = '上传: ${result.uploadedFiles} 文件, ${result.uploadedImages} 图片\n' final details = '上传: ${result.uploadedFiles} 文件, ${result.uploadedImages} 图片\n'
'下载: ${result.downloadedFiles} 文件, ${result.downloadedImages} 图片'; '下载: ${result.downloadedFiles} 文件, ${result.downloadedImages} 图片';
if (result.needReload) { if (result.needReload) {
ToastUtil.show(context, '数据已更新,正在重新加载...'); ToastUtil.show(context, '数据已更新,正在重新加载...');
@@ -129,39 +144,48 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
void _showResultDialog(String title, String content) { void _showResultDialog(String title, String content) {
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) {
backgroundColor: Colors.white, final colors = Theme.of(ctx).colorScheme;
elevation: 0, return AlertDialog(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), backgroundColor: colors.surface,
title: Column( elevation: 0,
children: [ shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
Container( title: Column(
width: 48, height: 48, children: [
decoration: BoxDecoration( Container(
color: const Color(0xFFF5F5F5), width: 48,
borderRadius: BorderRadius.circular(12), 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,
const SizedBox(height: 16), style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
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))),
), ),
], 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>( final selected = await showModalBottomSheet<int>(
context: context, context: context,
backgroundColor: Colors.white, backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), shape: const RoundedRectangleBorder(
builder: (ctx) => SafeArea( borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
child: Column( builder: (ctx) {
mainAxisSize: MainAxisSize.min, final colors = Theme.of(ctx).colorScheme;
children: [ return SafeArea(
const SizedBox(height: 8), child: Column(
Container(width: 36, height: 4, decoration: BoxDecoration(color: const Color(0xFFDDDDDD), borderRadius: BorderRadius.circular(2))), mainAxisSize: MainAxisSize.min,
const SizedBox(height: 12), children: [
const Text('同步间隔', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(height: 8),
const SizedBox(height: 8), Container(
...intervals.map((i) => ListTile( width: 36,
title: Text('$i 分钟', style: TextStyle(fontSize: 15, fontWeight: _autoSyncInterval == i ? FontWeight.w600 : FontWeight.w400, color: const Color(0xFF1A1A1A))), height: 4,
trailing: _autoSyncInterval == i ? const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 20) : null, decoration: BoxDecoration(
onTap: () => Navigator.pop(ctx, i), color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
)), const SizedBox(height: 12),
const SizedBox(height: 8), 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) { if (selected != null && selected != _autoSyncInterval) {
@@ -226,41 +265,54 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
Future<void> _clearConfig() async { Future<void> _clearConfig() async {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) {
backgroundColor: Colors.white, final colors = Theme.of(ctx).colorScheme;
elevation: 0, return AlertDialog(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), backgroundColor: colors.surface,
title: Row( elevation: 0,
children: [ shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
Container( title: Row(
width: 40, height: 40, children: [
decoration: BoxDecoration(color: Colors.red.withOpacity(0.08), borderRadius: BorderRadius.circular(10)), Container(
child: const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22), 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) { if (confirmed == true) {
@@ -281,46 +333,54 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: colors.surface,
appBar: AppBar(title: const Text('WebDAV 备份')), appBar: AppBar(title: const Text('WebDAV 备份')),
body: _isLoading && !_isConfigured body: _isLoading && !_isConfigured
? const Center(child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A))) ? Center(child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary))
: ListView( : ListView(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
children: [ children: [
const SizedBox(height: 8), const SizedBox(height: 8),
// 已连接提示 // 已连接提示
if (_isConfigured) _buildConnectedBanner(), if (_isConfigured) _buildConnectedBanner(colors),
// 服务器配置 // 服务器配置
_buildSectionLabel('服务器配置'), _buildSectionLabel(colors, '服务器配置'),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildInput( _buildInput(
colors: colors,
controller: _urlController, controller: _urlController,
hint: '服务器地址,如 https://dav.example.com', hint: '服务器地址,如 https://dav.example.com',
icon: Icons.link, icon: Icons.link,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildInput( _buildInput(
colors: colors,
controller: _usernameController, controller: _usernameController,
hint: '用户名', hint: '用户名',
icon: Icons.person_outline, icon: Icons.person_outline,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildInput( _buildInput(
colors: colors,
controller: _passwordController, controller: _passwordController,
hint: '密码', hint: '密码',
icon: Icons.lock_outline, icon: Icons.lock_outline,
obscure: _obscurePassword, obscure: _obscurePassword,
suffix: GestureDetector( suffix: GestureDetector(
onTap: () => setState(() => _obscurePassword = !_obscurePassword), 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), const SizedBox(height: 12),
_buildInput( _buildInput(
colors: colors,
controller: _pathController, controller: _pathController,
hint: '同步路径,如 /mooknote', hint: '同步路径,如 /mooknote',
icon: Icons.folder_outlined, icon: Icons.folder_outlined,
@@ -328,14 +388,15 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
const SizedBox(height: 24), const SizedBox(height: 24),
// 测试并保存 // 测试并保存
_buildBtn('测试并保存', onTap: _isLoading ? null : _saveConfig), _buildBtn(colors, '测试并保存', onTap: _isLoading ? null : _saveConfig),
const SizedBox(height: 40), const SizedBox(height: 40),
if (_isConfigured) ...[ if (_isConfigured) ...[
// 自动同步 // 自动同步
_buildSectionLabel('自动同步'), _buildSectionLabel(colors, '自动同步'),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildSwitchRow( _buildSwitchRow(
colors: colors,
icon: Icons.sync, icon: Icons.sync,
label: '自动同步', label: '自动同步',
sub: _isAutoSyncEnabled ? '$_autoSyncInterval 分钟自动同步' : '关闭', sub: _isAutoSyncEnabled ? '$_autoSyncInterval 分钟自动同步' : '关闭',
@@ -345,6 +406,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
if (_isAutoSyncEnabled) ...[ if (_isAutoSyncEnabled) ...[
const SizedBox(height: 4), const SizedBox(height: 4),
_buildTappableRow( _buildTappableRow(
colors: colors,
label: '同步间隔', label: '同步间隔',
value: '$_autoSyncInterval 分钟', value: '$_autoSyncInterval 分钟',
onTap: _isLoading ? null : _showIntervalPicker, onTap: _isLoading ? null : _showIntervalPicker,
@@ -354,27 +416,31 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
const SizedBox(height: 32), const SizedBox(height: 32),
// 手动同步 // 手动同步
_buildSectionLabel('手动同步'), _buildSectionLabel(colors, '手动同步'),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
children: [ children: [
Expanded(child: _buildDirectionChip('上传到云端', SyncDirection.upload, Icons.upload)), Expanded(
child: _buildDirectionChip(
colors, '上传到云端', SyncDirection.upload, Icons.upload)),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(child: _buildDirectionChip('下载到本地', SyncDirection.download, Icons.download)), Expanded(
child: _buildDirectionChip(
colors, '下载到本地', SyncDirection.download, Icons.download)),
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
_buildBtn('立即同步', onTap: _isLoading ? null : _syncData, loading: _isLoading), _buildBtn(colors, '立即同步', onTap: _isLoading ? null : _syncData, loading: _isLoading),
const SizedBox(height: 32), const SizedBox(height: 32),
// 清除配置 // 清除配置
_buildTextBtn('清除配置', onTap: _isLoading ? null : _clearConfig), _buildTextBtn(colors, '清除配置', onTap: _isLoading ? null : _clearConfig),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
const SizedBox(height: 32), const SizedBox(height: 32),
_buildTips(), _buildTips(colors),
const SizedBox(height: 60), const SizedBox(height: 60),
], ],
), ),
@@ -383,40 +449,48 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
// ── widgets ───────────────────────────────────────── // ── widgets ─────────────────────────────────────────
Widget _buildConnectedBanner() { Widget _buildConnectedBanner(ColorScheme colors) {
return Container( return Container(
margin: const EdgeInsets.only(bottom: 24), margin: const EdgeInsets.only(bottom: 24),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Row( child: Row(
children: [ children: [
Container( Container(
width: 6, height: 6, width: 6,
height: 6,
decoration: const BoxDecoration(color: Color(0xFF4CAF50), shape: BoxShape.circle), decoration: const BoxDecoration(color: Color(0xFF4CAF50), shape: BoxShape.circle),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Text( child: Text(
_urlController.text, _urlController.text,
style: const TextStyle(fontSize: 13, color: Color(0xFF666666)), style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6)),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, 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) { Widget _buildSectionLabel(ColorScheme colors, String text) {
return Text(text, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF999999), letterSpacing: 0.5)); return Text(text,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: colors.onSurface.withValues(alpha: 0.4),
letterSpacing: 0.5));
} }
Widget _buildInput({ Widget _buildInput({
required ColorScheme colors,
required TextEditingController controller, required TextEditingController controller,
required String hint, required String hint,
required IconData icon, required IconData icon,
@@ -426,35 +500,37 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
return TextField( return TextField(
controller: controller, controller: controller,
obscureText: obscure, obscureText: obscure,
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)), style: TextStyle(fontSize: 15, color: colors.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
hintText: hint, hintText: hint,
hintStyle: const TextStyle(fontSize: 15, color: Color(0xFFBBBBBB)), hintStyle: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.3)),
prefixIcon: Padding( prefixIcon: Padding(
padding: const EdgeInsets.only(left: 4, right: 8), 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), prefixIconConstraints: const BoxConstraints(minWidth: 44),
suffixIcon: suffix != null ? Padding( suffixIcon: suffix != null
padding: const EdgeInsets.only(right: 8), ? Padding(
child: suffix, padding: const EdgeInsets.only(right: 8),
) : null, child: suffix,
)
: null,
filled: true, filled: true,
fillColor: const Color(0xFFF8F8F8), fillColor: colors.surfaceContainerHigh,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), 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), 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; final disabled = onTap == null;
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
@@ -462,30 +538,44 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 15), padding: const EdgeInsets.symmetric(vertical: 15),
decoration: BoxDecoration( decoration: BoxDecoration(
color: disabled ? const Color(0xFFDDDDDD) : const Color(0xFF1A1A1A), color: disabled ? colors.onSurface.withValues(alpha: 0.15) : colors.primary,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Center( child: Center(
child: loading child: loading
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white))) ? SizedBox(
: Text(text, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white)), 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( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Center( child: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12), 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({ Widget _buildSwitchRow({
required ColorScheme colors,
required IconData icon, required IconData icon,
required String label, required String label,
required String sub, required String sub,
@@ -495,26 +585,30 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF8F8F8), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Row( child: Row(
children: [ 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), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
value ? sub : label, 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( Switch(
value: value, value: value,
onChanged: onChanged, onChanged: onChanged,
activeColor: const Color(0xFF1A1A1A), activeThumbColor: colors.primary,
activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), activeTrackColor: colors.primary.withValues(alpha: 0.3),
inactiveThumbColor: Colors.white, inactiveThumbColor: colors.surface,
inactiveTrackColor: const Color(0xFFDDDDDD), inactiveTrackColor: colors.onSurface.withValues(alpha: 0.15),
), ),
], ],
), ),
@@ -522,6 +616,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
} }
Widget _buildTappableRow({ Widget _buildTappableRow({
required ColorScheme colors,
required String label, required String label,
required String value, required String value,
VoidCallback? onTap, VoidCallback? onTap,
@@ -531,77 +626,99 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF8F8F8), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Row( child: Row(
children: [ children: [
const SizedBox(width: 32), 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(), 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 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; final selected = _syncDirection == dir;
return GestureDetector( return GestureDetector(
onTap: () => setState(() => _syncDirection = dir), onTap: () => setState(() => _syncDirection = dir),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFF8F8F8), color: selected ? colors.primary : colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), 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( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF8F8F8), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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), const SizedBox(height: 10),
_tip('坚果云、Nextcloud、AList 等 WebDAV 服务'), _tip(colors, '坚果云、Nextcloud、AList 等 WebDAV 服务'),
_tip('服务器地址需包含 https://'), _tip(colors, '服务器地址需包含 https://'),
_tip('首次同步可能需要较长时间'), _tip(colors, '首次同步可能需要较长时间'),
], ],
), ),
); );
} }
Widget _tip(String text) { Widget _tip(ColorScheme colors, String text) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.only(bottom: 6),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Padding( Padding(
padding: EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Icon(Icons.circle, size: 4, color: Color(0xFFCCCCCC)), child: Icon(Icons.circle,
size: 4, color: colors.onSurface.withValues(alpha: 0.25)),
), ),
const SizedBox(width: 8), 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]; String get _currentType => _tabTypes[_currentIndex];
/// 计算标签使用次数
Map<String, int> _getTagUsageCounts(String type) { Map<String, int> _getTagUsageCounts(String type) {
final provider = context.read<AppProvider>(); final provider = context.read<AppProvider>();
final counts = <String, int>{}; final counts = <String, int>{};
@@ -81,16 +80,17 @@ class _TagManagementPageState extends State<TagManagementPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F8F8), backgroundColor: colors.surfaceContainerHigh,
appBar: AppBar( appBar: AppBar(
title: const Text('标签管理'), title: const Text('标签管理'),
actions: [ actions: [
_isSyncing _isSyncing
? const Padding( ? Padding(
padding: EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: SizedBox(width: 20, height: 20, child: SizedBox(width: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A))), child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)),
) )
: IconButton( : IconButton(
icon: const Icon(Icons.sync, size: 20), icon: const Icon(Icons.sync, size: 20),
@@ -112,7 +112,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), color: colors.primary,
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 12, offset: const Offset(0, 4)), 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( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.add, size: 18, color: Colors.white), Icon(Icons.add, size: 18, color: colors.onPrimary),
const SizedBox(width: 6), const SizedBox(width: 6),
Text('添加${_typeLabels[_currentIndex]}', 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() { Widget _buildTabSelector() {
final colors = Theme.of(context).colorScheme;
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 24), margin: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFEBEBEB), color: colors.outlineVariant,
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
), ),
child: LayoutBuilder( child: LayoutBuilder(
@@ -158,7 +157,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
padding: const EdgeInsets.all(3), padding: const EdgeInsets.all(3),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(22),
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, 2)), 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, alignment: Alignment.center,
child: Text(_typeLabels[i], child: Text(_typeLabels[i],
style: TextStyle(fontSize: 13, fontWeight: selected ? FontWeight.w600 : FontWeight.w500, 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) { Widget _buildTagList(String type) {
final colors = Theme.of(context).colorScheme;
final tags = _tagCache[type] ?? []; final tags = _tagCache[type] ?? [];
final usageCounts = _getTagUsageCounts(type); final usageCounts = _getTagUsageCounts(type);
@@ -207,7 +205,6 @@ class _TagManagementPageState extends State<TagManagementPage> {
return _buildEmptyState(type); return _buildEmptyState(type);
} }
// 按使用次数降序排序
final sorted = List<Map<String, dynamic>>.from(tags) final sorted = List<Map<String, dynamic>>.from(tags)
..sort((a, b) { ..sort((a, b) {
final ca = usageCounts[a['name']] ?? 0; final ca = usageCounts[a['name']] ?? 0;
@@ -220,18 +217,16 @@ class _TagManagementPageState extends State<TagManagementPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 统计行
Padding( Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12),
child: Row( child: Row(
children: [ 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(), 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( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 80), 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) { Widget _buildTagChip(Map<String, dynamic> tag, Map<String, int> usageCounts) {
final colors = Theme.of(context).colorScheme;
final name = tag['name'] as String; final name = tag['name'] as String;
final count = usageCounts[name] ?? 0; final count = usageCounts[name] ?? 0;
return Container( return GestureDetector(
padding: const EdgeInsets.only(left: 14, right: 6, top: 8, bottom: 8), onLongPress: () => _showRenameDialog(tag),
decoration: BoxDecoration( child: Container(
color: Colors.white, padding: const EdgeInsets.only(left: 14, right: 6, top: 8, bottom: 8),
borderRadius: BorderRadius.circular(20), decoration: BoxDecoration(
boxShadow: [ color: colors.surface,
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: const Offset(0, 2)), 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: [ child: Row(
// 标签名区域 — 点击改名 mainAxisSize: MainAxisSize.min,
GestureDetector( children: [
onTap: () => _showRenameDialog(tag), Text(name, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface)),
child: Text(name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), if (count > 0) ...[
), const SizedBox(width: 6),
if (count > 0) ...[ Container(
const SizedBox(width: 6), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
Container( decoration: BoxDecoration(color: colors.outlineVariant, borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), child: Text('$count', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.4))),
decoration: BoxDecoration(color: const Color(0xFFF0F0F0), borderRadius: BorderRadius.circular(8)), ),
child: Text('$count', style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: Color(0xFF999999))), ],
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) { Widget _buildEmptyState(String type) {
final colors = Theme.of(context).colorScheme;
final idx = _tabTypes.indexOf(type); final idx = _tabTypes.indexOf(type);
final icon = _typeIcons[idx]; final icon = _typeIcons[idx];
final label = _typeLabels[idx]; final label = _typeLabels[idx];
@@ -303,57 +298,56 @@ class _TagManagementPageState extends State<TagManagementPage> {
children: [ children: [
Container( Container(
width: 64, height: 64, width: 64, height: 64,
decoration: BoxDecoration(color: const Color(0xFFECECEC), borderRadius: BorderRadius.circular(18)), decoration: BoxDecoration(color: colors.outlineVariant, borderRadius: BorderRadius.circular(18)),
child: Icon(icon, size: 28, color: const Color(0xFFCCCCCC)), child: Icon(icon, size: 28, color: colors.onSurface.withValues(alpha: 0.25)),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text('暂无$label', 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), 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() { void _showAddDialog() {
final colors = Theme.of(context).colorScheme;
final controller = TextEditingController(); final controller = TextEditingController();
final type = _currentType; final type = _currentType;
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
backgroundColor: Colors.white, backgroundColor: colors.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Text('添加${_typeLabels[_currentIndex]}', 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( content: TextField(
controller: controller, autofocus: true, controller: controller, autofocus: true,
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)), style: TextStyle(fontSize: 15, color: colors.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
hintText: '输入标签名称', hintText: '输入标签名称',
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)), hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
filled: true, fillColor: const Color(0xFFFAFAFA), filled: true, fillColor: colors.surfaceContainerHigh,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), 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), onSubmitted: (value) => _doAddTag(ctx, controller.text.trim(), type),
), ),
actions: [ 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( Container(
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(20)), decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)),
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: () => _doAddTag(ctx, controller.text.trim(), type), onTap: () => _doAddTag(ctx, controller.text.trim(), type),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
child: const Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text('添加', style: TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500)), child: Text('添加', style: TextStyle(fontSize: 14, color: colors.onPrimary, fontWeight: FontWeight.w500)),
), ),
), ),
), ),
@@ -378,9 +372,8 @@ class _TagManagementPageState extends State<TagManagementPage> {
await _loadTags(type); await _loadTags(type);
} }
// ─── 重命名 ──────────────────────────────────────────────────────────
void _showRenameDialog(Map<String, dynamic> tag) { void _showRenameDialog(Map<String, dynamic> tag) {
final colors = Theme.of(context).colorScheme;
final controller = TextEditingController(text: tag['name'] as String); final controller = TextEditingController(text: tag['name'] as String);
final tagId = tag['id'] as String; final tagId = tag['id'] as String;
final type = tag['type'] as String; final type = tag['type'] as String;
@@ -389,34 +382,34 @@ class _TagManagementPageState extends State<TagManagementPage> {
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
backgroundColor: Colors.white, backgroundColor: colors.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), 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( content: TextField(
controller: controller, autofocus: true, controller: controller, autofocus: true,
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)), style: TextStyle(fontSize: 15, color: colors.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
hintText: '输入新名称', hintText: '输入新名称',
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)), hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
filled: true, fillColor: const Color(0xFFFAFAFA), filled: true, fillColor: colors.surfaceContainerHigh,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), 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), onSubmitted: (value) => _doRenameTag(ctx, tagId, value.trim(), type, oldName),
), ),
actions: [ 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( Container(
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(20)), decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)),
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: () => _doRenameTag(ctx, tagId, controller.text.trim(), type, oldName), onTap: () => _doRenameTag(ctx, tagId, controller.text.trim(), type, oldName),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
child: const Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text('确定', style: TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500)), 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); if (success) await _loadTags(type);
} }
// ─── 删除(长按触发)──────────────────────────────────────────────────
void _showDeleteDialog(Map<String, dynamic> tag) { void _showDeleteDialog(Map<String, dynamic> tag) {
final colors = Theme.of(context).colorScheme;
final tagId = tag['id'] as String; final tagId = tag['id'] as String;
final type = tag['type'] as String; final type = tag['type'] as String;
final name = tag['name'] as String; final name = tag['name'] as String;
@@ -457,17 +449,17 @@ class _TagManagementPageState extends State<TagManagementPage> {
context: context, context: context,
builder: (ctx) => StatefulBuilder( builder: (ctx) => StatefulBuilder(
builder: (ctx, setDialogState) => AlertDialog( builder: (ctx, setDialogState) => AlertDialog(
backgroundColor: Colors.white, backgroundColor: colors.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row( title: Row(
children: [ children: [
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(12)), decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(12)),
child: Text(name, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))), child: Text(name, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6))),
), ),
const SizedBox(width: 10), 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), titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
@@ -481,7 +473,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 4), 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), const SizedBox(height: 12),
_buildDeleteOption(value: 'remove', groupValue: selectedAction, onChanged: (v) => setDialogState(() => selectedAction = v), title: '从所有条目中移除该标签', subtitle: '标签将从影视/书籍/笔记中清除'), _buildDeleteOption(value: 'remove', groupValue: selectedAction, onChanged: (v) => setDialogState(() => selectedAction = v), title: '从所有条目中移除该标签', subtitle: '标签将从影视/书籍/笔记中清除'),
const SizedBox(height: 4), const SizedBox(height: 4),
@@ -504,19 +496,19 @@ class _TagManagementPageState extends State<TagManagementPage> {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5), color: isSelected ? colors.primary : colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16), 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(), }).toList(),
) )
: Container( : Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(12)), decoration: BoxDecoration(color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(12)),
child: const Text('无其他标签可替换', style: TextStyle(fontSize: 13, color: Color(0xFFAAAAAA))), 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), contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
actions: [ 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( Container(
decoration: BoxDecoration(color: const Color(0xFFE53935), borderRadius: BorderRadius.circular(20)), decoration: BoxDecoration(color: const Color(0xFFE53935), borderRadius: BorderRadius.circular(20)),
child: Material( child: Material(
@@ -573,15 +565,16 @@ class _TagManagementPageState extends State<TagManagementPage> {
required String title, required String title,
String? subtitle, String? subtitle,
}) { }) {
final colors = Theme.of(context).colorScheme;
final selected = value == groupValue; final selected = value == groupValue;
return GestureDetector( return GestureDetector(
onTap: () => onChanged(value), onTap: () => onChanged(value),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected ? const Color(0xFFFAFAFA) : Colors.white, color: selected ? colors.surfaceContainerHigh : colors.surface,
borderRadius: BorderRadius.circular(12), 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( child: Row(
children: [ children: [
@@ -589,14 +582,14 @@ class _TagManagementPageState extends State<TagManagementPage> {
width: 18, height: 18, width: 18, height: 18,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, 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), const SizedBox(width: 12),
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ 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))), 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: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)))), 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: 笔记) // 当前主界面选中的标签 (0: 观影1: 阅读2: 笔记)
int _mainTabIndex = 0; int _mainTabIndex = 0;
// 当前底部导航选中的索引 (0: 主页1: 新增2: 我的) // 当前底部导航选中的索引 (0: 主页1: 新增2: 我的)
int _bottomNavIndex = 0; int _bottomNavIndex = 0;
// 底部导航栏是否可见 // 底部导航栏是否可见
bool _bottomNavVisible = true; bool _bottomNavVisible = true;
// 主题模式
ThemeMode _themeMode = ThemeMode.system;
/// 是否使用远程服务端(同步开关 + 已激活) /// 是否使用远程服务端(同步开关 + 已激活)
bool get _useRemote { bool get _useRemote {
final prefs = UserPrefs(); final prefs = UserPrefs();
@@ -134,6 +137,7 @@ class AppProvider extends ChangeNotifier {
int get bookStatusIndex => _bookStatusIndex; int get bookStatusIndex => _bookStatusIndex;
bool get drawerOpen => _drawerOpen; bool get drawerOpen => _drawerOpen;
bool get bottomNavVisible => _bottomNavVisible; bool get bottomNavVisible => _bottomNavVisible;
ThemeMode get themeMode => _themeMode;
List<Movie> get movies => _movies; List<Movie> get movies => _movies;
List<Book> get books => _books; List<Book> get books => _books;
List<Note> get notes => _notes; 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) { void setMovieStatusIndex(int index) {
_movieStatusIndex = index; _movieStatusIndex = index;
notifyListeners(); notifyListeners();

View File

@@ -51,6 +51,10 @@ class AppTheme {
onSurface: _black, onSurface: _black,
error: error, error: error,
onError: _white, onError: _white,
surfaceContainerHighest: _offWhite,
surfaceContainerHigh: Color(0xFFFAFAFA),
outline: _lighterGray,
outlineVariant: Color(0xFFF0F0F0),
), ),
// AppBar - 极简无边框 // AppBar - 极简无边框
@@ -262,6 +266,10 @@ class AppTheme {
onSurface: _white, onSurface: _white,
error: Color(0xFFEF4444), error: Color(0xFFEF4444),
onError: _black, onError: _black,
surfaceContainerHighest: _darkGray,
surfaceContainerHigh: Color(0xFF2A2A2A),
outline: _gray,
outlineVariant: _darkGray,
), ),
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
@@ -286,6 +294,12 @@ class AppTheme {
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
), ),
listTileTheme: const ListTileThemeData(
contentPadding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
minLeadingWidth: 0,
dense: true,
),
dividerTheme: DividerThemeData( dividerTheme: DividerThemeData(
color: _darkGray, color: _darkGray,
@@ -304,6 +318,9 @@ class AppTheme {
focusedBorder: UnderlineInputBorder( focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: _white, width: 1), borderSide: BorderSide(color: _white, width: 1),
), ),
errorBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFFEF4444), width: 0.5),
),
contentPadding: EdgeInsets.symmetric(vertical: 12), contentPadding: EdgeInsets.symmetric(vertical: 12),
hintStyle: TextStyle( hintStyle: TextStyle(
fontFamily: _fontFamily, fontFamily: _fontFamily,
@@ -311,6 +328,12 @@ class AppTheme {
fontWeight: _regular, fontWeight: _regular,
color: _gray, color: _gray,
), ),
labelStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 13,
fontWeight: _medium,
color: _lightGray,
),
), ),
elevatedButtonTheme: ElevatedButtonThemeData( elevatedButtonTheme: ElevatedButtonThemeData(
@@ -327,6 +350,11 @@ class AppTheme {
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: _white, foregroundColor: _white,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
textStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 14,
fontWeight: _medium,
),
), ),
), ),
@@ -335,6 +363,84 @@ class AppTheme {
selectedItemColor: _white, selectedItemColor: _white,
unselectedItemColor: _gray, unselectedItemColor: _gray,
elevation: 0, 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(); 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; Timer? _heartbeatTimer;
bool _started = false; bool _started = false;

View File

@@ -11,6 +11,11 @@ class UserPrefs {
/// 初始化 /// 初始化
static Future<void> init() async { static Future<void> init() async {
_prefs = await SharedPreferences.getInstance(); _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 {
// ========== 应用设置 ========== // ========== 应用设置 ==========
/// 是否暗黑模式 /// 主题模式: 0=跟随系统, 1=浅色, 2=深色
bool get isDarkMode => prefs.getBool('isDarkMode') ?? false; int get themeMode => prefs.getInt('themeMode') ?? 0;
Future<bool> setDarkMode(bool value) => prefs.setBool('isDarkMode', value); Future<bool> setThemeMode(int value) => prefs.setInt('themeMode', value);
/// 是否首次启动 /// 是否首次启动
bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true; bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true;

View File

@@ -55,6 +55,7 @@ class _AnimatedStarRatingState extends State<AnimatedStarRating>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final starValue = widget.rating / 2; final starValue = widget.rating / 2;
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -84,7 +85,7 @@ class _AnimatedStarRatingState extends State<AnimatedStarRating>
style: TextStyle( style: TextStyle(
fontSize: widget.starSize, fontSize: widget.starSize,
fontWeight: FontWeight.w600, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return RefreshIndicator( return RefreshIndicator(
onRefresh: onRefresh, onRefresh: onRefresh,
color: const Color(0xFF1A1A1A), color: colors.primary,
backgroundColor: Colors.white, backgroundColor: colors.surface,
strokeWidth: 2.5, strokeWidth: 2.5,
displacement: 60, displacement: 60,
semanticsLabel: semanticsLabel, semanticsLabel: semanticsLabel,

View File

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

View File

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

View File

@@ -33,36 +33,25 @@ class _CustomDrawerState extends State<CustomDrawer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Drawer( return Drawer(
backgroundColor: const Color(0xFFF8F8F8), backgroundColor: colors.surfaceContainerHigh,
child: SafeArea( child: SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 头像 + 统计(独立卡片区域)
_buildProfileCard(context), _buildProfileCard(context),
const SizedBox(height: 16), const SizedBox(height: 16),
// 热力图
_buildCalendarSection(context), _buildCalendarSection(context),
const SizedBox(height: 16), const SizedBox(height: 16),
// 最近添加
_buildRecentSection(context), _buildRecentSection(context),
const SizedBox(height: 16), const SizedBox(height: 16),
// 功能入口
_buildToolsCard(context), _buildToolsCard(context),
// 底部版本号
Padding( Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 32), padding: const EdgeInsets.fromLTRB(20, 20, 20, 32),
child: Center( 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) { Widget _buildProfileCard(BuildContext context) {
return Consumer<AppProvider>( return Consumer<AppProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
final colors = Theme.of(context).colorScheme;
final userPrefs = UserPrefs(); final userPrefs = UserPrefs();
final nickname = userPrefs.nickname; final nickname = userPrefs.nickname;
final motto = userPrefs.motto; final motto = userPrefs.motto;
@@ -89,77 +79,69 @@ class _CustomDrawerState extends State<CustomDrawer> {
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0), margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 头像 + 名称/座右铭 + 统计
Row( Row(
children: [ children: [
// 头像
Container( Container(
width: 52, width: 52,
height: 52, height: 52,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5), border: Border.all(color: colors.outlineVariant, width: 0.5),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: avatarPath != null && avatarPath.isNotEmpty child: avatarPath != null && avatarPath.isNotEmpty
? Image.file(File(avatarPath), fit: BoxFit.cover, ? Image.file(File(avatarPath), fit: BoxFit.cover,
errorBuilder: (_, __, ___) => errorBuilder: (_, __, ___) =>
const Icon(Icons.person_outline, size: 26, color: Color(0xFFBBBBBB))) Icon(Icons.person_outline, size: 26, color: colors.onSurface.withValues(alpha: 0.3)))
: const Icon(Icons.person_outline, size: 26, color: Color(0xFFBBBBBB)), : Icon(Icons.person_outline, size: 26, color: colors.onSurface.withValues(alpha: 0.3)),
), ),
const SizedBox(width: 14), const SizedBox(width: 14),
// 名称 + 座右铭
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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), const SizedBox(height: 2),
Text(motto, maxLines: 1, overflow: TextOverflow.ellipsis, 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 SizedBox(height: 16),
const Divider(height: 1, color: Color(0xFFF0F0F0)), Divider(height: 1, color: colors.outlineVariant),
const SizedBox(height: 14), const SizedBox(height: 14),
// 统计数字
_buildProfileStatRow(Icons.movie_outlined, movieCount, '观影'), _buildProfileStatRow(Icons.movie_outlined, movieCount, '观影'),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildProfileStatRow(Icons.menu_book_outlined, bookCount, '阅读'), _buildProfileStatRow(Icons.menu_book_outlined, bookCount, '阅读'),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildProfileStatRow(Icons.note_outlined, noteCount, '笔记'), _buildProfileStatRow(Icons.note_outlined, noteCount, '笔记'),
// 设置入口
const SizedBox(height: 14), const SizedBox(height: 14),
const Divider(height: 1, color: Color(0xFFF0F0F0)), Divider(height: 1, color: colors.outlineVariant),
InkWell( InkWell(
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsPage())); Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsPage()));
}, },
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: const Padding( child: Padding(
padding: EdgeInsets.only(top: 14), padding: const EdgeInsets.only(top: 14),
child: Row( child: Row(
children: [ children: [
Icon(Icons.settings_outlined, size: 16, color: Color(0xFF999999)), Icon(Icons.settings_outlined, size: 16, color: colors.onSurface.withValues(alpha: 0.4)),
SizedBox(width: 8), const SizedBox(width: 8),
Text('设置', style: TextStyle(fontSize: 13, color: Color(0xFF888888))), Text('设置', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
Spacer(), const Spacer(),
Icon(Icons.chevron_right, size: 14, color: Color(0xFFD0D0D0)), 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) { Widget _buildProfileStatRow(IconData icon, int count, String label) {
final colors = Theme.of(context).colorScheme;
return Row( return Row(
children: [ children: [
Icon(icon, size: 16, color: const Color(0xFF888888)), Icon(icon, size: 16, color: colors.onSurface.withValues(alpha: 0.5)),
const SizedBox(width: 10), 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(), 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) { Widget _buildToolsCard(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Column( child: Column(
@@ -204,12 +188,12 @@ class _CustomDrawerState extends State<CustomDrawer> {
Navigator.pop(context); Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage())); Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage()));
}, topRounded: true), }, 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, '标签管理', () { _buildToolItem(Icons.label_outline, '标签管理', () {
Navigator.pop(context); Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (_) => const TagManagementPage())); 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阅读', () { _buildToolItem(Icons.description_outlined, 'MD阅读', () {
Navigator.pop(context); Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (_) => const MdReaderTabPage())); 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}) { Widget _buildToolItem(IconData icon, String title, VoidCallback onTap, {bool topRounded = false, bool bottomRounded = false}) {
final colors = Theme.of(context).colorScheme;
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
@@ -232,21 +217,22 @@ class _CustomDrawerState extends State<CustomDrawer> {
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20), padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20),
child: Row( child: Row(
children: [ children: [
Icon(icon, size: 20, color: const Color(0xFF555555)), Icon(icon, size: 20, color: colors.onSurface.withValues(alpha: 0.7)),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(child: Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))), Expanded(child: Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface))),
const Icon(Icons.chevron_right, size: 16, color: Color(0xFFD0D0D0)), Icon(Icons.chevron_right, size: 16, color: colors.onSurface.withValues(alpha: 0.2)),
], ],
), ),
), ),
); );
} }
// ─── 热力图(保持现状) ────────────────────────────────────────────── // ─── 热力图 ──────────────────────────────────────────────────────────
Widget _buildCalendarSection(BuildContext context) { Widget _buildCalendarSection(BuildContext context) {
return Consumer<AppProvider>( return Consumer<AppProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
final colors = Theme.of(context).colorScheme;
final Map<DateTime, int> dailyCounts = {}; final Map<DateTime, int> dailyCounts = {};
for (final movie in provider.movies.where((m) => !m.isDeleted)) { for (final movie in provider.movies.where((m) => !m.isDeleted)) {
final date = DateTime(movie.createdAt.year, movie.createdAt.month, movie.createdAt.day); final date = DateTime(movie.createdAt.year, movie.createdAt.month, movie.createdAt.day);
@@ -299,15 +285,15 @@ class _CustomDrawerState extends State<CustomDrawer> {
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(18), 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Row( Row(
children: [ children: [
Icon(Icons.calendar_today, size: 14, color: Color(0xFF999999)), Icon(Icons.calendar_today, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
SizedBox(width: 8), const SizedBox(width: 8),
Text('热力图', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))), Text('热力图', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6))),
], ],
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
@@ -324,7 +310,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
final label = keepWeeks.contains(week) ? monthLabels[week] : null; final label = keepWeeks.contains(week) ? monthLabels[week] : null;
return SizedBox( return SizedBox(
width: cellSize + cellGap, 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( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ 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), const SizedBox(width: 3),
_legendCell(const Color(0xFFF0F0F0)), _legendCell(const Color(0xFFF0F0F0)),
_legendCell(const Color(0xFFC8E6C9)), _legendCell(const Color(0xFFC8E6C9)),
@@ -355,7 +341,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
_legendCell(const Color(0xFF2E7D32)), _legendCell(const Color(0xFF2E7D32)),
_legendCell(const Color(0xFF1B5E20)), _legendCell(const Color(0xFF1B5E20)),
const SizedBox(width: 3), 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) { Widget _buildRecentSection(BuildContext context) {
return Consumer<AppProvider>( return Consumer<AppProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
final colors = Theme.of(context).colorScheme;
final recent = _getRecentItems(provider); final recent = _getRecentItems(provider);
if (recent.isEmpty) return const SizedBox.shrink(); if (recent.isEmpty) return const SizedBox.shrink();
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(18), 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Row( Row(
children: [ children: [
Icon(Icons.schedule, size: 14, color: Color(0xFF999999)), Icon(Icons.schedule, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
SizedBox(width: 8), const SizedBox(width: 8),
Text('最近添加', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))), Text('最近添加', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6))),
], ],
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
@@ -408,15 +395,15 @@ class _CustomDrawerState extends State<CustomDrawer> {
children: [ children: [
Icon( Icon(
item.type == 'movie' ? Icons.movie_outlined : item.type == 'book' ? Icons.menu_book_outlined : Icons.note_outlined, 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), const SizedBox(width: 10),
Expanded( Expanded(
child: Text(item.title, maxLines: 1, overflow: TextOverflow.ellipsis, 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), 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; return;
} }
// 如果是 http 开头,直接当网络图片
if (widget.path!.startsWith('http')) { if (widget.path!.startsWith('http')) {
_useNetwork = true; _useNetwork = true;
_imageUrl = widget.path; _imageUrl = widget.path;
@@ -60,7 +59,6 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
return; return;
} }
// 本地文件存在就直接显示
final file = File(widget.path!); final file = File(widget.path!);
if (file.existsSync()) { if (file.existsSync()) {
setState(() => _loaded = true); setState(() => _loaded = true);
@@ -68,7 +66,6 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
return; return;
} }
// 本地不存在,尝试服务端 URL
if (ServerDataService.isActive) { if (ServerDataService.isActive) {
try { try {
final url = await ServerDataService.toImageUrl(widget.path!); final url = await ServerDataService.toImageUrl(widget.path!);
@@ -105,13 +102,14 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
if (_error) { if (_error) {
return widget.errorWidget ?? return widget.errorWidget ??
Container( Container(
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
child: const Icon(Icons.broken_image_outlined, size: 24, color: Color(0xFFCCCCCC)), child: Icon(Icons.broken_image_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25)),
); );
} }
if (!_loaded) { if (!_loaded) {
@@ -119,7 +117,7 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
Container( Container(
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
); );
} }
return FadeTransition( return FadeTransition(
@@ -136,8 +134,8 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
Container( Container(
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
child: const Icon(Icons.broken_image_outlined, size: 24, color: Color(0xFFCCCCCC)), 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( Container(
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
color: const Color(0xFFF5F5F5), color: colors.surfaceContainerHighest,
child: const Icon(Icons.broken_image_outlined, size: 24, color: Color(0xFFCCCCCC)), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return InkWell( return InkWell(
onTap: () { onTap: () {
Navigator.pushNamed(context, '/movie-detail', arguments: movie); Navigator.pushNamed(context, '/movie-detail', arguments: movie);
@@ -23,28 +24,21 @@ class MovieListItem extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 海报
Expanded( Expanded(
child: _buildPoster(), child: _buildPoster(colors),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// 影视名称
Text( Text(
movie.title, movie.title,
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A), color: Theme.of(context).colorScheme.onSurface,
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
// 评分
if (movie.rating != null) if (movie.rating != null)
AnimatedStarRating(rating: movie.rating!, starSize: 12, showNumber: true) AnimatedStarRating(rating: movie.rating!, starSize: 12, showNumber: true)
else else
@@ -53,45 +47,45 @@ class MovieListItem extends StatelessWidget {
), ),
); );
} }
/// 构建海报 Widget _buildPoster(ColorScheme colors) {
Widget _buildPoster() {
return Container( return Container(
width: double.infinity, width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), 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, clipBehavior: Clip.antiAlias,
child: FadeInLocalImage( child: FadeInLocalImage(
path: movie.posterPath, path: movie.posterPath,
fit: BoxFit.cover, fit: BoxFit.cover,
placeholder: 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: const Center(child: Icon(Icons.movie_outlined, size: 24, color: Color(0xFFCCCCCC))), errorWidget: Center(child: Icon(Icons.movie_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25))),
), ),
); );
} }
/// 显示删除确认对话框
void _showDeleteDialog(BuildContext context) { void _showDeleteDialog(BuildContext context) {
final colors = Theme.of(context).colorScheme;
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: Colors.white, backgroundColor: colors.surface,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text( title: Text(
'确认删除', '确认删除',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: colors.onSurface,
), ),
), ),
content: Text( content: Text(
'确定要删除《${movie.title}》吗?删除后可在回收站恢复。', '确定要删除《${movie.title}》吗?删除后可在回收站恢复。',
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xFF666666), color: colors.onSurface.withValues(alpha: 0.6),
height: 1.5, height: 1.5,
), ),
), ),
@@ -99,7 +93,7 @@ class MovieListItem extends StatelessWidget {
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: const Color(0xFF666666), foregroundColor: colors.onSurface.withValues(alpha: 0.6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
), ),
child: const Text('取消'), child: const Text('取消'),
@@ -111,8 +105,8 @@ class MovieListItem extends StatelessWidget {
ToastUtil.show(context, '已删除'); ToastUtil.show(context, '已删除');
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: colors.error,
foregroundColor: Colors.white, foregroundColor: colors.onError,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),

View File

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

View File

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