generated from dellevin/template
适配深色模式
This commit is contained in:
131
lib/main.dart
131
lib/main.dart
@@ -1,8 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'dart:async';
|
||||
import 'pages/home_page.dart';
|
||||
import 'utils/theme/app_theme.dart';
|
||||
import 'utils/app_router.dart';
|
||||
@@ -13,31 +14,17 @@ import 'providers/app_provider.dart';
|
||||
import 'package:flutter/widget_previews.dart';
|
||||
|
||||
void main() async {
|
||||
// 确保 Flutter 绑定初始化完成
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// 设置系统导航栏颜色(与App主题一致)
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
systemNavigationBarColor: Colors.white,
|
||||
systemNavigationBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
// 初始化用户偏好设置
|
||||
await UserPrefs.init();
|
||||
final appProvider = AppProvider();
|
||||
// 先显示界面,后台加载数据
|
||||
runApp(MyApp(appProvider: appProvider));
|
||||
unawaited(appProvider.initDatabase().then((_) => appProvider.initMainTabIndex()));
|
||||
unawaited(_initAutoBackup());
|
||||
unawaited(_initUsageStats());
|
||||
}
|
||||
|
||||
/// 初始化自动备份
|
||||
Future<void> _initAutoBackup() async {
|
||||
try {
|
||||
// 初始化本地自动备份
|
||||
final isLocalAutoBackupEnabled = await AutoBackupService.instance.getEnabled();
|
||||
if (isLocalAutoBackupEnabled) {
|
||||
await AutoBackupService.instance.start();
|
||||
@@ -47,7 +34,6 @@ Future<void> _initAutoBackup() async {
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化匿名用户统计
|
||||
Future<void> _initUsageStats() async {
|
||||
try {
|
||||
await UsageStatsService.instance.start();
|
||||
@@ -56,51 +42,92 @@ Future<void> _initUsageStats() async {
|
||||
}
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
class MyApp extends StatefulWidget {
|
||||
final AppProvider appProvider;
|
||||
|
||||
|
||||
const MyApp({super.key, required this.appProvider});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
widget.appProvider.loadThemeMode();
|
||||
_updateSystemUI(widget.appProvider.themeMode);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_updateSystemUI(widget.appProvider.themeMode);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateSystemUI(ThemeMode mode) {
|
||||
final Brightness brightness;
|
||||
switch (mode) {
|
||||
case ThemeMode.light:
|
||||
brightness = Brightness.light;
|
||||
case ThemeMode.dark:
|
||||
brightness = Brightness.dark;
|
||||
case ThemeMode.system:
|
||||
brightness = PlatformDispatcher.instance.platformBrightness;
|
||||
}
|
||||
final isDark = brightness == Brightness.dark;
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor: isDark ? const Color(0xFF1A1A1A) : Colors.white,
|
||||
systemNavigationBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 获取当前选中的图标名称
|
||||
final iconName = UserPrefs().appIconName;
|
||||
|
||||
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: appProvider),
|
||||
ChangeNotifierProvider.value(value: widget.appProvider),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'MookNote',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: ThemeMode.system,
|
||||
localizationsDelegates: [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [
|
||||
Locale('zh', 'CN'),
|
||||
Locale('en', 'US'),
|
||||
],
|
||||
home: const HomePage(),
|
||||
onGenerateRoute: AppRouter.generateRoute,
|
||||
builder: (context, child) {
|
||||
// 尝试动态设置应用图标(Android 13+ 支持动态图标,但 Flutter 目前主要通过静态配置)
|
||||
// 这里我们主要实现逻辑上的切换,实际生效通常需要重启应用或配合原生插件
|
||||
return _AppIconWrapper(iconName: iconName, child: child!);
|
||||
child: Consumer<AppProvider>(
|
||||
builder: (context, provider, _) {
|
||||
_updateSystemUI(provider.themeMode);
|
||||
return MaterialApp(
|
||||
title: 'MookNote',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: provider.themeMode,
|
||||
localizationsDelegates: [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [
|
||||
Locale('zh', 'CN'),
|
||||
Locale('en', 'US'),
|
||||
],
|
||||
home: const HomePage(),
|
||||
onGenerateRoute: AppRouter.generateRoute,
|
||||
builder: (context, child) {
|
||||
return _AppIconWrapper(iconName: iconName, child: child!);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用图标包装器
|
||||
/// 注意:Flutter 默认不支持运行时动态更换桌面图标。
|
||||
/// 这里的实现主要是为了在应用内记录用户的选择,并为未来可能的动态图标功能做准备。
|
||||
/// 如果需要真正的动态图标,通常需要引入 flutter_app_icon_changer 等插件并配置多套图标资源。
|
||||
class _AppIconWrapper extends StatefulWidget {
|
||||
final Widget child;
|
||||
final String iconName;
|
||||
@@ -118,11 +145,7 @@ class _AppIconWrapperState extends State<_AppIconWrapper> {
|
||||
_updateSystemIcon();
|
||||
}
|
||||
|
||||
Future<void> _updateSystemIcon() async {
|
||||
// 目前 Flutter 官方不支持直接通过代码更换 Launcher Icon。
|
||||
// 这一步主要用于记录日志或在未来集成第三方库时使用。
|
||||
// print('Current selected icon: ${widget.iconName}');
|
||||
}
|
||||
Future<void> _updateSystemIcon() async {}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -130,11 +153,9 @@ class _AppIconWrapperState extends State<_AppIconWrapper> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 用于预览 MyApp 的 Widget
|
||||
/// 添加 @Preview 注解
|
||||
@Preview(name: "MookNote App Preview")
|
||||
Widget previewMyApp() {
|
||||
final appProvider = AppProvider();
|
||||
final appProvider = AppProvider();
|
||||
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
@@ -142,4 +163,4 @@ Widget previewMyApp() {
|
||||
],
|
||||
child: MyApp(appProvider: appProvider),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
||||
final UserPrefs _userPrefs = UserPrefs();
|
||||
String _currentIconName = 'app_icon';
|
||||
|
||||
// 预定义的图标列表
|
||||
final List<Map<String, String>> _icons = [
|
||||
{'name': 'app_icon', 'label': '默认图标'},
|
||||
{'name': 'app_icon2', 'label': '风格二'},
|
||||
@@ -28,7 +27,6 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
||||
}
|
||||
|
||||
Future<void> _loadCurrentIcon() async {
|
||||
// 先从原生层获取当前实际启用的图标(更准确)
|
||||
final nativeIcon = await AppIconChannel.getCurrentIcon();
|
||||
setState(() {
|
||||
_currentIconName = nativeIcon;
|
||||
@@ -39,7 +37,6 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
||||
if (iconName == _currentIconName) return;
|
||||
|
||||
try {
|
||||
// 调用原生层切换桌面图标
|
||||
final success = await AppIconChannel.switchIcon(iconName);
|
||||
|
||||
if (success) {
|
||||
@@ -65,8 +62,9 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('应用图标'),
|
||||
),
|
||||
@@ -82,22 +80,21 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
||||
itemBuilder: (context, index) {
|
||||
final icon = _icons[index];
|
||||
final isSelected = _currentIconName == icon['name'];
|
||||
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _selectIcon(icon['name']!),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFFF0F0F0) : const Color(0xFFFAFAFA),
|
||||
color: isSelected ? colors.outlineVariant : colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFE8E8E8),
|
||||
color: isSelected ? colors.primary : colors.outlineVariant,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 图标预览
|
||||
Image.asset(
|
||||
'assets/icon/${icon['name']}.png',
|
||||
width: 64,
|
||||
@@ -107,26 +104,25 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEEEEEE),
|
||||
color: colors.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(Icons.image_not_supported, color: Color(0xFF999999)),
|
||||
child: Icon(Icons.image_not_supported, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 标签
|
||||
Text(
|
||||
icon['label']!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
if (isSelected) ...[
|
||||
const SizedBox(height: 4),
|
||||
const Icon(Icons.check_circle, size: 18, color: Color(0xFF1A1A1A)),
|
||||
Icon(Icons.check_circle, size: 18, color: colors.primary),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -13,9 +13,9 @@ import 'book_share_page.dart';
|
||||
/// 书籍详情页 - 极简主义设计
|
||||
class BookDetailPage extends StatefulWidget {
|
||||
final Book book;
|
||||
|
||||
|
||||
const BookDetailPage({super.key, required this.book});
|
||||
|
||||
|
||||
@override
|
||||
State<BookDetailPage> createState() => _BookDetailPageState();
|
||||
}
|
||||
@@ -24,80 +24,54 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
// 页面获得焦点时刷新数据
|
||||
_refreshBookData();
|
||||
}
|
||||
|
||||
void _refreshBookData() {
|
||||
final provider = context.read<AppProvider>();
|
||||
// 强制刷新当前书籍数据
|
||||
provider.loadBooks();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 从 Provider 获取最新的 book 数据,实现动态刷新
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final book = context.watch<AppProvider>().books
|
||||
.where((b) => b.id == widget.book.id)
|
||||
.firstOrNull ?? widget.book;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
body: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
// 顶部封面区域
|
||||
_buildSliverAppBar(book),
|
||||
|
||||
// 内容区域
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 基本信息
|
||||
_buildBasicInfo(book),
|
||||
|
||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
||||
|
||||
// 作者信息
|
||||
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||
_buildAuthorsSection(book),
|
||||
|
||||
// 类型
|
||||
if (book.genres.isNotEmpty)
|
||||
_buildGenresSection(book),
|
||||
|
||||
// ISBN
|
||||
if (book.isbn != null && book.isbn!.isNotEmpty)
|
||||
_buildIsbnSection(book),
|
||||
|
||||
// 出版社
|
||||
if (book.publisher != null && book.publisher!.isNotEmpty)
|
||||
_buildPublisherSection(book),
|
||||
|
||||
// 出版时间
|
||||
if (book.publishDate != null)
|
||||
_buildPublishDateSection(book),
|
||||
|
||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
||||
|
||||
// 简介
|
||||
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||
if (book.summary != null && book.summary!.isNotEmpty)
|
||||
_buildSummarySection(book),
|
||||
|
||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
||||
|
||||
// 书评和摘抄入口
|
||||
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||
_buildExtraSections(book),
|
||||
|
||||
const SizedBox(height: 120),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 右下角悬浮按钮组
|
||||
Positioned(
|
||||
right: 16,
|
||||
bottom: 24,
|
||||
@@ -107,9 +81,9 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建右下角悬浮按钮组
|
||||
|
||||
Widget _buildFloatingActionButtons(Book book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -117,13 +91,16 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
icon: Icons.edit_outlined,
|
||||
onPressed: () => _navigateToEdit(context),
|
||||
tooltip: '编辑',
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildFloatingButton(
|
||||
icon: Icons.delete_outline,
|
||||
onPressed: () => _showDeleteDialog(context),
|
||||
tooltip: '删除',
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: colors.error,
|
||||
foregroundColor: colors.onError,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildFloatingButton(
|
||||
@@ -131,17 +108,18 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
onPressed: () => _showSharePoster(book),
|
||||
tooltip: '分享海报',
|
||||
backgroundColor: const Color(0xFF4CAF50),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建单个悬浮按钮
|
||||
Widget _buildFloatingButton({
|
||||
required IconData icon,
|
||||
required VoidCallback onPressed,
|
||||
required String tooltip,
|
||||
Color backgroundColor = const Color(0xFF1A1A1A),
|
||||
required Color backgroundColor,
|
||||
required Color foregroundColor,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
@@ -160,15 +138,15 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
],
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(icon, size: 18, color: Colors.white),
|
||||
icon: Icon(icon, size: 18, color: foregroundColor),
|
||||
onPressed: onPressed,
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: tooltip,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建带背景的返回按钮
|
||||
Widget _buildBackButton() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||
@@ -194,21 +172,19 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建顶部 AppBar
|
||||
Widget _buildSliverAppBar(Book book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return SliverAppBar(
|
||||
expandedHeight: 320,
|
||||
pinned: true,
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
backgroundColor: colors.surfaceContainerHighest,
|
||||
leading: _buildBackButton(),
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: _buildCoverSection(book),
|
||||
),
|
||||
// 右上角按钮已移到右下角悬浮按钮
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建封面区域
|
||||
Widget _buildCoverSection(Book book) {
|
||||
return SizedBox.expand(
|
||||
child: book.coverPath != null && book.coverPath!.isNotEmpty
|
||||
@@ -220,79 +196,74 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
: _buildCoverPlaceholder(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildCoverPlaceholder() {
|
||||
return const Center(
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.menu_book,
|
||||
size: 64,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'暂无封面',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建基本信息
|
||||
|
||||
Widget _buildBasicInfo(Book book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 书名
|
||||
Text(
|
||||
book.title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
|
||||
// 别名(显示在主名称下面,用 / 分隔)
|
||||
if (book.alternateTitles.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
book.alternateTitles.join(' / '),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 评分和状态
|
||||
Row(
|
||||
children: [
|
||||
if (book.rating != null) ...[
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.star,
|
||||
size: 20,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
book.rating!.toStringAsFixed(1),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@@ -300,15 +271,12 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
_buildStatusTag(book),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 时间信息
|
||||
Text(
|
||||
'添加于 ${_formatDate(book.createdAt)}',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -316,33 +284,33 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建状态标签
|
||||
Widget _buildStatusTag(Book book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
String label;
|
||||
Color bgColor;
|
||||
Color textColor;
|
||||
switch (book.status) {
|
||||
case 'read':
|
||||
label = '已读';
|
||||
bgColor = const Color(0xFF1A1A1A);
|
||||
textColor = Colors.white;
|
||||
bgColor = colors.primary;
|
||||
textColor = colors.onPrimary;
|
||||
break;
|
||||
case 'reading':
|
||||
label = '在读';
|
||||
bgColor = const Color(0xFFF0F0F0);
|
||||
textColor = const Color(0xFF666666);
|
||||
bgColor = colors.outlineVariant;
|
||||
textColor = colors.onSurface.withValues(alpha: 0.6);
|
||||
break;
|
||||
case 'want_to_read':
|
||||
label = '想读';
|
||||
bgColor = const Color(0xFFF5F5F5);
|
||||
textColor = const Color(0xFF999999);
|
||||
bgColor = colors.surfaceContainerHighest;
|
||||
textColor = colors.onSurface.withValues(alpha: 0.4);
|
||||
break;
|
||||
default:
|
||||
label = '未知';
|
||||
bgColor = const Color(0xFFEEEEEE);
|
||||
textColor = const Color(0xFFCCCCCC);
|
||||
bgColor = colors.outlineVariant;
|
||||
textColor = colors.onSurface.withValues(alpha: 0.25);
|
||||
}
|
||||
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
@@ -359,30 +327,30 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建作者区域
|
||||
|
||||
Widget _buildAuthorsSection(Book book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 64,
|
||||
child: Text(
|
||||
'作者',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
book.authors.join(','),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -392,29 +360,29 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建ISBN区域
|
||||
Widget _buildIsbnSection(Book book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 64,
|
||||
child: Text(
|
||||
'ISBN',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
book.isbn!,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -424,29 +392,29 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建出版社区域
|
||||
Widget _buildPublisherSection(Book book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 64,
|
||||
child: Text(
|
||||
'出版社',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
book.publisher!,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -456,29 +424,29 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建出版时间区域
|
||||
Widget _buildPublishDateSection(Book book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 64,
|
||||
child: Text(
|
||||
'出版时间',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${book.publishDate!.year}年${book.publishDate!.month.toString().padLeft(2, '0')}月',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -488,20 +456,20 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建类型区域
|
||||
Widget _buildGenresSection(Book book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 64,
|
||||
child: Text(
|
||||
'类型',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -513,14 +481,14 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
genre,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -531,9 +499,9 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建简介区域
|
||||
|
||||
Widget _buildSummarySection(Book book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
@@ -545,17 +513,17 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
width: 4,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
Text(
|
||||
'简介',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -564,14 +532,14 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
book.summary!,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.8,
|
||||
),
|
||||
),
|
||||
@@ -581,8 +549,8 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建额外功能区域(书评、摘抄)
|
||||
Widget _buildExtraSections(Book book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
@@ -594,23 +562,22 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
width: 4,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
Text(
|
||||
'更多',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 书评入口
|
||||
_buildExtraSectionItem(
|
||||
icon: Icons.rate_review_outlined,
|
||||
title: '书评',
|
||||
@@ -620,7 +587,6 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
onTap: () => _navigateToReviews(book),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 摘抄入口
|
||||
_buildExtraSectionItem(
|
||||
icon: Icons.format_quote_outlined,
|
||||
title: '摘抄',
|
||||
@@ -634,7 +600,6 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建更多区域项
|
||||
Widget _buildExtraSectionItem({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
@@ -643,14 +608,15 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
required String unit,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -658,14 +624,14 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: const Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -675,10 +641,10 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -688,9 +654,9 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
final count = snapshot.data ?? 0;
|
||||
return Text(
|
||||
count > 0 ? '$count $unit' : emptyText,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -698,9 +664,9 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -726,39 +692,37 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// 格式化日期
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 跳转到编辑页面
|
||||
|
||||
void _navigateToEdit(BuildContext context) {
|
||||
Navigator.pushNamed(context, '/book-form', arguments: widget.book).then((_) {
|
||||
context.read<AppProvider>().loadBooks();
|
||||
});
|
||||
}
|
||||
|
||||
/// 显示删除对话框
|
||||
|
||||
void _showDeleteDialog(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text(
|
||||
title: Text(
|
||||
'确认删除',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
content: Text(
|
||||
'确定要删除"${widget.book.title}"吗?删除后可在回收站恢复。',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -766,7 +730,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF666666),
|
||||
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
child: const Text('取消'),
|
||||
@@ -780,8 +744,8 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
ToastUtil.show(context, '已删除');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colors.error,
|
||||
foregroundColor: colors.onError,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -796,7 +760,6 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 下载封面到本地
|
||||
Future<void> _downloadCover(Book book) async {
|
||||
if (book.coverPath == null || book.coverPath!.isEmpty) {
|
||||
ToastUtil.show(context, '没有可下载的封面');
|
||||
@@ -810,16 +773,13 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成文件名:书籍名称_时间戳_封面.jpg
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final fileName = '${book.title}_${timestamp}_封面.jpg';
|
||||
|
||||
// 获取临时目录路径
|
||||
final tempDir = await Directory.systemTemp.createTemp();
|
||||
final tempFile = File('${tempDir.path}/$fileName');
|
||||
await sourceFile.copy(tempFile.path);
|
||||
|
||||
// 使用分享功能让用户选择保存位置
|
||||
await Share.shareXFiles(
|
||||
[XFile(tempFile.path)],
|
||||
subject: '${book.title} 封面',
|
||||
@@ -830,7 +790,6 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示分享海报页面
|
||||
void _showSharePoster(Book book) {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
||||
@@ -50,8 +50,9 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: Text(_isEditing ? '编辑摘抄' : '添加摘抄'),
|
||||
actions: [
|
||||
@@ -63,10 +64,10 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text(
|
||||
: Text(
|
||||
'保存',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@@ -82,15 +83,15 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
||||
// 章节
|
||||
TextFormField(
|
||||
controller: _chapterController,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
labelText: '章节(可选)',
|
||||
hintText: '例如:第一章、第3节等',
|
||||
border: OutlineInputBorder(
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide: BorderSide(color: Color(0xFF1A1A1A)),
|
||||
borderSide: BorderSide(color: colors.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -101,16 +102,16 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
||||
TextFormField(
|
||||
controller: _contentController,
|
||||
maxLines: 8,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
labelText: '摘抄内容',
|
||||
hintText: '输入你想要摘抄的内容...',
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide: BorderSide(color: Color(0xFF1A1A1A)),
|
||||
borderSide: BorderSide(color: colors.primary),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
@@ -127,16 +128,16 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
||||
TextFormField(
|
||||
controller: _commentController,
|
||||
maxLines: 5,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
labelText: '我的感悟(可选)',
|
||||
hintText: '记录你对这段内容的思考和感悟...',
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide: BorderSide(color: Color(0xFF1A1A1A)),
|
||||
borderSide: BorderSide(color: colors.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -185,4 +186,3 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,9 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('摘抄'),
|
||||
actions: [
|
||||
@@ -62,6 +63,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -70,21 +72,21 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.format_quote_outlined,
|
||||
size: 40,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
Text(
|
||||
'暂无摘抄',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -93,15 +95,15 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'添加记录',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
color: colors.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -114,7 +116,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
||||
/// 按章节分组摘抄数据
|
||||
Map<String, List<BookExcerpt>> _groupExcerptsByChapter() {
|
||||
final Map<String, List<BookExcerpt>> groups = {};
|
||||
|
||||
|
||||
for (final excerpt in _excerpts) {
|
||||
final chapter = excerpt.chapter.isEmpty ? '未分类' : excerpt.chapter;
|
||||
if (!groups.containsKey(chapter)) {
|
||||
@@ -122,19 +124,19 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
||||
}
|
||||
groups[chapter]!.add(excerpt);
|
||||
}
|
||||
|
||||
|
||||
// 每个章节内的摘抄按时间排序(新的在前)
|
||||
for (final chapter in groups.keys) {
|
||||
groups[chapter]!.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
}
|
||||
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
Widget _buildExcerptList() {
|
||||
final groups = _groupExcerptsByChapter();
|
||||
final chapters = groups.keys.toList();
|
||||
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: chapters.length,
|
||||
@@ -147,6 +149,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
||||
}
|
||||
|
||||
Widget _buildChapterSection(String chapter, List<BookExcerpt> excerpts) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -154,18 +157,18 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF5F5F5),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest,
|
||||
border: Border(
|
||||
left: BorderSide(color: Color(0xFF1A1A1A), width: 4),
|
||||
left: BorderSide(color: colors.primary, width: 4),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
chapter,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -178,11 +181,12 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
||||
}
|
||||
|
||||
Widget _buildExcerptItem(BookExcerpt excerpt) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
border: Border.all(color: colors.outline),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -192,9 +196,9 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
||||
excerpt.content,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -205,16 +209,16 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF5F5F5),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest,
|
||||
),
|
||||
child: Text(
|
||||
excerpt.comment,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -227,29 +231,29 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
||||
children: [
|
||||
Text(
|
||||
_formatDate(excerpt.createdAt),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// 编辑按钮
|
||||
GestureDetector(
|
||||
onTap: () => _navigateToEditExcerpt(excerpt),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.edit_outlined,
|
||||
size: 16,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 删除按钮
|
||||
GestureDetector(
|
||||
onTap: () => _showDeleteDialog(excerpt),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.delete_outline,
|
||||
size: 16,
|
||||
color: Colors.red,
|
||||
color: colors.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -287,28 +291,31 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
||||
void _showDeleteDialog(BookExcerpt excerpt) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除这条摘抄吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeBookExcerpt(excerpt.id);
|
||||
Navigator.pop(context);
|
||||
_loadExcerpts();
|
||||
ToastUtil.show(context, '已删除');
|
||||
},
|
||||
child: const Text('删除', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除这条摘抄吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeBookExcerpt(excerpt.id);
|
||||
Navigator.pop(context);
|
||||
_loadExcerpts();
|
||||
ToastUtil.show(context, '已删除');
|
||||
},
|
||||
child: Text('删除', style: TextStyle(color: colors.error)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -49,8 +49,9 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('书评详情'),
|
||||
actions: [
|
||||
@@ -70,9 +71,9 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
||||
// 书评内容
|
||||
Text(
|
||||
_review.content,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.8,
|
||||
),
|
||||
),
|
||||
@@ -82,7 +83,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
||||
// 分隔线
|
||||
Container(
|
||||
height: 0.5,
|
||||
color: const Color(0xFFE5E5E5),
|
||||
color: colors.outline,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
@@ -92,6 +93,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
||||
icon: Icons.person_outline,
|
||||
label: '书评人:',
|
||||
value: _review.reviewer.isNotEmpty ? _review.reviewer : '匿名',
|
||||
colors: colors,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
@@ -102,6 +104,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
||||
icon: Icons.source_outlined,
|
||||
label: '来源:',
|
||||
value: _review.source,
|
||||
colors: colors,
|
||||
),
|
||||
|
||||
if (_review.source.isNotEmpty) const SizedBox(height: 16),
|
||||
@@ -111,6 +114,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
||||
icon: Icons.category_outlined,
|
||||
label: '类型:',
|
||||
value: _review.typeText,
|
||||
colors: colors,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
@@ -120,6 +124,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
||||
icon: Icons.access_time,
|
||||
label: '时间:',
|
||||
value: _formatDate(_review.createdAt),
|
||||
colors: colors,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -132,29 +137,30 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
required ColorScheme colors,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: const Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -47,10 +47,11 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final isEdit = widget.review != null;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: Text(isEdit ? '编辑书评' : '写书评'),
|
||||
actions: [
|
||||
@@ -74,24 +75,24 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
||||
// 顶部信息栏
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
|
||||
bottom: BorderSide(color: colors.outline, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 类型选择
|
||||
_buildTypeSelector(),
|
||||
_buildTypeSelector(colors),
|
||||
const SizedBox(width: 16),
|
||||
// 评论人
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _reviewerController,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
||||
decoration: const InputDecoration(
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: '评论人',
|
||||
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
|
||||
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@@ -104,10 +105,10 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
||||
width: 100,
|
||||
child: TextField(
|
||||
controller: _sourceController,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
||||
decoration: const InputDecoration(
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: '来源',
|
||||
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
|
||||
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@@ -125,19 +126,19 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.7,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
hintText: '写下你的书评...',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.all(16),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
@@ -154,29 +155,29 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
||||
}
|
||||
|
||||
/// 构建类型选择器
|
||||
Widget _buildTypeSelector() {
|
||||
Widget _buildTypeSelector(ColorScheme colors) {
|
||||
return GestureDetector(
|
||||
onTap: () => _showTypeSelector(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
border: Border.all(color: colors.outline),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_reviewType == 1 ? '短评' : '长评',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.arrow_drop_down,
|
||||
size: 16,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -188,36 +189,42 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
||||
void _showTypeSelector() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
builder: (context) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('短评'),
|
||||
trailing: _reviewType == 1
|
||||
? const Icon(Icons.check, color: Color(0xFF1A1A1A))
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() => _reviewType = 1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
builder: (context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
color: colors.surface,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('短评'),
|
||||
trailing: _reviewType == 1
|
||||
? Icon(Icons.check, color: colors.onSurface)
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() => _reviewType = 1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
Divider(height: 0.5, color: colors.outline),
|
||||
ListTile(
|
||||
title: const Text('长评'),
|
||||
trailing: _reviewType == 2
|
||||
? Icon(Icons.check, color: colors.onSurface)
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() => _reviewType = 2);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 0.5),
|
||||
ListTile(
|
||||
title: const Text('长评'),
|
||||
trailing: _reviewType == 2
|
||||
? const Icon(Icons.check, color: Color(0xFF1A1A1A))
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() => _reviewType = 2);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -259,4 +266,3 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,19 +73,20 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: _isSearching
|
||||
? TextField(
|
||||
controller: _searchController,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索书评内容、书评人、来源...',
|
||||
hintStyle: TextStyle(color: Color(0xFF999999)),
|
||||
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: const TextStyle(color: Color(0xFF1A1A1A)),
|
||||
style: TextStyle(color: colors.onSurface),
|
||||
onChanged: _onSearchChanged,
|
||||
)
|
||||
: const Text('书评'),
|
||||
@@ -112,6 +113,7 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -120,21 +122,21 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.rate_review_outlined,
|
||||
size: 40,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
Text(
|
||||
'暂无书评',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -143,15 +145,15 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'添加记录',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
color: colors.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -176,13 +178,14 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
||||
}
|
||||
|
||||
Widget _buildReviewCard(BookReview review) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () => _navigateToReviewDetail(review),
|
||||
onLongPress: () => _showDeleteDialog(review),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
@@ -193,8 +196,8 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: review.reviewType == 1
|
||||
? Colors.white
|
||||
: const Color(0xFF1A1A1A),
|
||||
? colors.surface
|
||||
: colors.primary,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
@@ -202,8 +205,8 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: review.reviewType == 1
|
||||
? const Color(0xFF666666)
|
||||
: Colors.white,
|
||||
? colors.onSurface.withValues(alpha: 0.6)
|
||||
: colors.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -215,9 +218,9 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
||||
review.content,
|
||||
maxLines: review.reviewType == 1 ? 4 : 8,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -232,9 +235,9 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
review.reviewer,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -252,9 +255,9 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
review.source,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -262,9 +265,9 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
||||
// 日期
|
||||
Text(
|
||||
_formatDate(review.createdAt),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -315,28 +318,31 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
||||
void _showDeleteDialog(BookReview review) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除这条书评吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeBookReview(review.id);
|
||||
Navigator.pop(context);
|
||||
_loadReviews();
|
||||
ToastUtil.show(context, '已删除');
|
||||
},
|
||||
child: const Text('删除', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除这条书评吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeBookReview(review.id);
|
||||
Navigator.pop(context);
|
||||
_loadReviews();
|
||||
ToastUtil.show(context, '已删除');
|
||||
},
|
||||
child: Text('删除', style: TextStyle(color: colors.error)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,21 +23,22 @@ class _BookSharePageState extends State<BookSharePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
backgroundColor: colors.surfaceContainerHighest,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)),
|
||||
icon: Icon(Icons.close, color: colors.onSurface),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
title: Text(
|
||||
'分享海报',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
@@ -50,12 +51,12 @@ class _BookSharePageState extends State<BookSharePage> {
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text(
|
||||
: Text(
|
||||
'分享',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -76,13 +77,14 @@ class _BookSharePageState extends State<BookSharePage> {
|
||||
|
||||
/// 构建海报 Widget
|
||||
Widget _buildPosterWidget() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final book = widget.book;
|
||||
final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty;
|
||||
|
||||
return Container(
|
||||
width: 320,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -117,10 +119,10 @@ class _BookSharePageState extends State<BookSharePage> {
|
||||
// 书名
|
||||
Text(
|
||||
book.title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -129,9 +131,9 @@ class _BookSharePageState extends State<BookSharePage> {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
book.alternateTitles.join(' / '),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -157,11 +159,11 @@ class _BookSharePageState extends State<BookSharePage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
Text(
|
||||
'/ 10',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -171,44 +173,45 @@ class _BookSharePageState extends State<BookSharePage> {
|
||||
|
||||
// 作者
|
||||
if (book.authors.isNotEmpty)
|
||||
_buildInfoRow('作者', book.authors.join(' / ')),
|
||||
_buildInfoRow('作者', book.authors.join(' / '), colors),
|
||||
|
||||
// 出版社
|
||||
if (book.publisher != null && book.publisher!.isNotEmpty)
|
||||
_buildInfoRow('出版社', book.publisher!),
|
||||
_buildInfoRow('出版社', book.publisher!, colors),
|
||||
|
||||
// 出版时间
|
||||
if (book.publishDate != null)
|
||||
_buildInfoRow(
|
||||
'出版',
|
||||
'${book.publishDate!.year}.${book.publishDate!.month.toString().padLeft(2, '0')}.${book.publishDate!.day.toString().padLeft(2, '0')}',
|
||||
colors,
|
||||
),
|
||||
|
||||
// 类型
|
||||
if (book.genres.isNotEmpty)
|
||||
_buildInfoRow('类型', book.genres.join(' / ')),
|
||||
_buildInfoRow('类型', book.genres.join(' / '), colors),
|
||||
|
||||
// ISBN
|
||||
if (book.isbn != null && book.isbn!.isNotEmpty)
|
||||
_buildInfoRow('ISBN', book.isbn!),
|
||||
_buildInfoRow('ISBN', book.isbn!, colors),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 简介
|
||||
if (book.summary != null && book.summary!.isNotEmpty) ...[
|
||||
const Text(
|
||||
Text(
|
||||
'简介',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
book.summary!,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
height: 1.6,
|
||||
),
|
||||
maxLines: 5,
|
||||
@@ -219,7 +222,7 @@ class _BookSharePageState extends State<BookSharePage> {
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 底部标识
|
||||
const Divider(height: 1, color: Color(0xFFE8E8E8)),
|
||||
Divider(height: 1, color: colors.outline),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -227,14 +230,14 @@ class _BookSharePageState extends State<BookSharePage> {
|
||||
Icon(
|
||||
Icons.book_outlined,
|
||||
size: 14,
|
||||
color: const Color(0xFF1A1A1A).withOpacity(0.5),
|
||||
color: colors.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'来自 MookNote',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: const Color(0xFF1A1A1A).withOpacity(0.5),
|
||||
color: colors.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -248,7 +251,7 @@ class _BookSharePageState extends State<BookSharePage> {
|
||||
}
|
||||
|
||||
/// 构建信息行
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
Widget _buildInfoRow(String label, String value, ColorScheme colors) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
@@ -256,17 +259,17 @@ class _BookSharePageState extends State<BookSharePage> {
|
||||
children: [
|
||||
Text(
|
||||
'$label:',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF333333),
|
||||
color: colors.onSurface.withValues(alpha: 0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -17,7 +17,7 @@ class BookTabPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BookTabPageState extends State<BookTabPage> {
|
||||
int _layoutStyle = 0; // 0: 封面网格, 1: 列表
|
||||
int _layoutStyle = 0;
|
||||
bool _firstLoad = true;
|
||||
|
||||
@override
|
||||
@@ -42,6 +42,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
}
|
||||
|
||||
Widget _buildBookList(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'};
|
||||
@@ -55,8 +56,8 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
if (books.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await provider.loadBooks(),
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: Colors.white,
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.surface,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [_buildEmptyState(context, provider.bookStatusIndex)],
|
||||
@@ -73,10 +74,11 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
}
|
||||
|
||||
Widget _buildGridView(List books, AppProvider provider) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await provider.loadBooks(),
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: Colors.white,
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.surface,
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 100),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
@@ -92,10 +94,11 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
}
|
||||
|
||||
Widget _buildListView(List books, AppProvider provider) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await provider.loadBooks(),
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: Colors.white,
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.surface,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
||||
itemCount: books.length,
|
||||
@@ -105,6 +108,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
}
|
||||
|
||||
Widget _buildListCard(book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.pushNamed(context, '/book-detail', arguments: book),
|
||||
onLongPress: () => _showDeleteDialog(context, book),
|
||||
@@ -112,23 +116,22 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 封面缩略图
|
||||
Container(
|
||||
width: 48, height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F0F0),
|
||||
color: colors.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: book.coverPath != null && book.coverPath!.isNotEmpty
|
||||
? Image.file(File(book.coverPath!), fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const Icon(Icons.menu_book_outlined, size: 22, color: Color(0xFFCCCCCC)))
|
||||
: const Icon(Icons.menu_book_outlined, size: 22, color: Color(0xFFCCCCCC)),
|
||||
errorBuilder: (_, __, ___) => Icon(Icons.menu_book_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)))
|
||||
: Icon(Icons.menu_book_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -136,11 +139,11 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(book.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
if (book.authors.isNotEmpty) ...[
|
||||
const SizedBox(height: 3),
|
||||
Text(book.authors.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
|
||||
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
],
|
||||
const SizedBox(height: 6),
|
||||
if (book.rating != null)
|
||||
@@ -151,7 +154,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
|
||||
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -159,19 +162,20 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
}
|
||||
|
||||
void _showDeleteDialog(BuildContext context, book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
content: Text('确定要删除《${book.title}》吗?删除后可在回收站恢复。',
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)),
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
@@ -179,7 +183,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red, foregroundColor: Colors.white, elevation: 0,
|
||||
backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
@@ -193,11 +197,12 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
|
||||
Widget _buildSkeleton() {
|
||||
return _layoutStyle == 1
|
||||
? MovieSkeletonGrid() // reuse same grid skeleton pattern
|
||||
? MovieSkeletonGrid()
|
||||
: const BookSkeletonGrid();
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context, int statusIndex) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final statusText = ['已读', '在读', '想读'][statusIndex];
|
||||
return Center(
|
||||
child: Column(
|
||||
@@ -205,11 +210,11 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
children: [
|
||||
Container(
|
||||
width: 80, height: 80,
|
||||
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(20)),
|
||||
child: const Icon(Icons.menu_book_outlined, size: 40, color: Color(0xFFCCCCCC)),
|
||||
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)),
|
||||
child: Icon(Icons.menu_book_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text('暂无$statusText的书籍', style: const TextStyle(fontSize: 16, color: Color(0xFF999999))),
|
||||
Text('暂无$statusText的书籍', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const SizedBox(height: 24),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
@@ -218,8 +223,8 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(8)),
|
||||
child: const Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(8)),
|
||||
child: Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -99,7 +99,7 @@ class _HomePageState extends State<HomePage> {
|
||||
width: 44,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.horizontal(
|
||||
right: Radius.circular(28)),
|
||||
boxShadow: [
|
||||
@@ -111,10 +111,10 @@ class _HomePageState extends State<HomePage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Center(
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Color(0xFF999999),
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -31,7 +31,6 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
_loadTabSettings();
|
||||
}
|
||||
|
||||
/// 加载标签显示设置
|
||||
void _loadTabSettings() {
|
||||
setState(() {
|
||||
_showMovieTab = _userPrefs.showMovieTab;
|
||||
@@ -43,11 +42,9 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
// 当页面重新获得焦点时刷新设置
|
||||
_loadTabSettings();
|
||||
}
|
||||
|
||||
/// 获取启用的标签列表
|
||||
List<_TabItem> get _enabledTabs {
|
||||
final tabs = <_TabItem>[];
|
||||
if (_showMovieTab) tabs.add(_TabItem('影视', 0));
|
||||
@@ -56,7 +53,6 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
/// 将原始索引映射到启用标签的索引
|
||||
int _mapToEnabledTabIndex(int originalIndex) {
|
||||
final tabs = _enabledTabs;
|
||||
for (int i = 0; i < tabs.length; i++) {
|
||||
@@ -69,13 +65,8 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// 顶部 AppBar
|
||||
_buildAppBar(context),
|
||||
|
||||
// 三个标签页的标题栏
|
||||
_buildTabBar(context),
|
||||
|
||||
// 标签页内容
|
||||
Expanded(
|
||||
child: _buildTabContent(),
|
||||
),
|
||||
@@ -83,16 +74,13 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建顶部 AppBar
|
||||
Widget _buildAppBar(BuildContext context) {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return AppBar(
|
||||
title: Text(_getAppBarTitle(provider)),
|
||||
actions: [
|
||||
// 云备份按钮
|
||||
_buildCloudSyncButton(context),
|
||||
// 搜索按钮
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () {
|
||||
@@ -110,8 +98,8 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建云备份按钮
|
||||
Widget _buildCloudSyncButton(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.cloud_sync_outlined),
|
||||
tooltip: '云备份',
|
||||
@@ -128,21 +116,21 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.cloud_upload_outlined,
|
||||
size: 18,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
Text(
|
||||
'上传数据',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -156,21 +144,21 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.cloud_download_outlined,
|
||||
size: 18,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
Text(
|
||||
'下载数据',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -185,21 +173,21 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.settings_outlined,
|
||||
size: 18,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
Text(
|
||||
'WebDAV设置',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -227,9 +215,8 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 执行同步操作
|
||||
Future<void> _performSync(BuildContext context, SyncDirection direction) async {
|
||||
// 检查是否已配置
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final config = await WebDAVService.instance.getConfig();
|
||||
if (config == null) {
|
||||
if (context.mounted) {
|
||||
@@ -243,28 +230,24 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载对话框
|
||||
if (context.mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(
|
||||
builder: (context) => Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 执行同步
|
||||
final result = await WebDAVService.instance.syncData(direction: direction);
|
||||
|
||||
// 关闭加载对话框
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
// 下载了数据则刷新界面
|
||||
if (result.success && result.needReload && context.mounted) {
|
||||
final provider = context.read<AppProvider>();
|
||||
await provider.loadMovies();
|
||||
@@ -272,11 +255,10 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
await provider.loadNotes();
|
||||
}
|
||||
|
||||
// 显示结果
|
||||
if (context.mounted) {
|
||||
final isSuccess = result.success;
|
||||
final message = result.message.isNotEmpty ? result.message : (isSuccess ? '同步成功' : '同步失败');
|
||||
|
||||
|
||||
_showResultDialog(
|
||||
context,
|
||||
title: isSuccess ? '同步成功' : '同步失败',
|
||||
@@ -290,7 +272,6 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示同步结果对话框
|
||||
void _showResultDialog(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
@@ -298,10 +279,11 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
required bool isSuccess,
|
||||
Map<String, dynamic>? details,
|
||||
}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: Row(
|
||||
@@ -322,9 +304,10 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -335,9 +318,9 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
children: [
|
||||
Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -346,7 +329,7 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
@@ -370,8 +353,8 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF1A1A1A),
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -386,8 +369,8 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建详情行
|
||||
Widget _buildDetailRow(String label, String value, {bool isError = false}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
@@ -395,9 +378,9 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -405,7 +388,7 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isError ? const Color(0xFFE57373) : const Color(0xFF1A1A1A),
|
||||
color: isError ? const Color(0xFFE57373) : colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -413,7 +396,6 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取 AppBar 标题
|
||||
String _getAppBarTitle(AppProvider provider) {
|
||||
switch (provider.mainTabIndex) {
|
||||
case 0:
|
||||
@@ -440,17 +422,17 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建标签栏
|
||||
Widget _buildTabBar(BuildContext context) {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final tabs = _enabledTabs;
|
||||
final currentEnabledIndex = _mapToEnabledTabIndex(provider.mainTabIndex);
|
||||
final safeIndex = currentEnabledIndex < tabs.length ? currentEnabledIndex : 0;
|
||||
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -474,7 +456,7 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
Icon(
|
||||
_getTabIcon(tab.label),
|
||||
size: 22,
|
||||
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFB0B0B0),
|
||||
color: isSelected ? colors.primary : colors.onSurface.withValues(alpha: 0.35),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
@@ -482,7 +464,7 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
||||
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFB0B0B0),
|
||||
color: isSelected ? colors.primary : colors.onSurface.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -513,7 +495,7 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
width: indicatorWidth,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(1.5),
|
||||
),
|
||||
),
|
||||
@@ -524,7 +506,7 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
||||
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -532,12 +514,10 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建标签页内容
|
||||
Widget _buildTabContent() {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final tabs = _enabledTabs;
|
||||
// 找到当前应该显示的标签
|
||||
_TabItem? currentTab;
|
||||
for (final tab in tabs) {
|
||||
if (tab.originalIndex == provider.mainTabIndex) {
|
||||
@@ -545,10 +525,8 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 如果当前标签被禁用了,显示第一个启用的标签
|
||||
if (currentTab == null && tabs.isNotEmpty) {
|
||||
currentTab = tabs.first;
|
||||
// 更新 provider 的索引
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
provider.setMainTabIndex(currentTab!.originalIndex);
|
||||
});
|
||||
@@ -572,22 +550,21 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示添加对话框
|
||||
void _showAddDialog(BuildContext context, AppProvider provider) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
builder: (BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Wrap(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.movie, color: Color(0xFF1A1A1A)),
|
||||
leading: Icon(Icons.movie, color: colors.onSurface),
|
||||
title: const Text('添加观影'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// 根据当前影视标签页的选中状态设置默认值
|
||||
final statusMap = {
|
||||
0: 'watched',
|
||||
1: 'watching',
|
||||
@@ -601,13 +578,12 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(height: 0.5, indent: 56),
|
||||
Divider(height: 0.5, indent: 56, color: colors.outlineVariant),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.menu_book, color: Color(0xFF1A1A1A)),
|
||||
leading: Icon(Icons.menu_book, color: colors.onSurface),
|
||||
title: const Text('添加阅读'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// 根据当前阅读标签页的选中状态设置默认值
|
||||
final statusMap = {
|
||||
0: 'read',
|
||||
1: 'reading',
|
||||
@@ -621,9 +597,9 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(height: 0.5, indent: 56),
|
||||
Divider(height: 0.5, indent: 56, color: colors.outlineVariant),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.note, color: Color(0xFF1A1A1A)),
|
||||
leading: Icon(Icons.note, color: colors.onSurface),
|
||||
title: const Text('添加笔记'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
@@ -638,7 +614,6 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 标签项数据类
|
||||
class _TabItem {
|
||||
final String label;
|
||||
final int originalIndex;
|
||||
|
||||
@@ -90,26 +90,30 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
void _showPermissionDeniedDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('需要存储权限', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
content: const Text(
|
||||
'Android 11+ 需要在系统设置中授予"所有文件访问权限"才能读取目录中的 Markdown 文件。\n\n是否前往设置?',
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('取消', style: TextStyle(color: Color(0xFF999999))),
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
return AlertDialog(
|
||||
title: const Text('需要存储权限',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
content: Text(
|
||||
'Android 11+ 需要在系统设置中授予"所有文件访问权限"才能读取目录中的 Markdown 文件。\n\n是否前往设置?',
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
openAppSettings();
|
||||
},
|
||||
child: const Text('前往设置', style: TextStyle(color: Color(0xFF1A1A1A))),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
openAppSettings();
|
||||
},
|
||||
child: Text('前往设置', style: TextStyle(color: colors.onSurface)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,7 +125,10 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
for (final entity in dir.listSync(recursive: true, followLinks: false)) {
|
||||
if (entity is File) {
|
||||
final lower = p.basename(entity.path).toLowerCase();
|
||||
if (lower.endsWith('.md') || lower.endsWith('.markdown') || lower.endsWith('.mdown') || lower.endsWith('.txt')) {
|
||||
if (lower.endsWith('.md') ||
|
||||
lower.endsWith('.markdown') ||
|
||||
lower.endsWith('.mdown') ||
|
||||
lower.endsWith('.txt')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -135,13 +142,28 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
try {
|
||||
final dir = Directory(dirPath);
|
||||
if (!dir.existsSync()) return false;
|
||||
const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico', '.tiff', '.heic', '.webm'];
|
||||
const imageExts = [
|
||||
'.png',
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.gif',
|
||||
'.webp',
|
||||
'.bmp',
|
||||
'.svg',
|
||||
'.ico',
|
||||
'.tiff',
|
||||
'.heic',
|
||||
'.webm'
|
||||
];
|
||||
bool hasAnyFile = false;
|
||||
for (final entity in dir.listSync(recursive: true, followLinks: false)) {
|
||||
if (entity is File) {
|
||||
hasAnyFile = true;
|
||||
final lower = p.basename(entity.path).toLowerCase();
|
||||
if (lower.endsWith('.md') || lower.endsWith('.markdown') || lower.endsWith('.mdown') || lower.endsWith('.txt')) {
|
||||
if (lower.endsWith('.md') ||
|
||||
lower.endsWith('.markdown') ||
|
||||
lower.endsWith('.mdown') ||
|
||||
lower.endsWith('.txt')) {
|
||||
return false;
|
||||
}
|
||||
if (!imageExts.any((ext) => lower.endsWith(ext))) {
|
||||
@@ -193,9 +215,13 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
}
|
||||
} else if (entity is File) {
|
||||
final lower = name.toLowerCase();
|
||||
if (lower.endsWith('.md') || lower.endsWith('.markdown') || lower.endsWith('.mdown') || lower.endsWith('.txt')) {
|
||||
if (lower.endsWith('.md') ||
|
||||
lower.endsWith('.markdown') ||
|
||||
lower.endsWith('.mdown') ||
|
||||
lower.endsWith('.txt')) {
|
||||
final stat = entity.statSync();
|
||||
entries.add(_FileEntry(name: name, path: entity.path, isDir: false, size: stat.size));
|
||||
entries.add(_FileEntry(
|
||||
name: name, path: entity.path, isDir: false, size: stat.size));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,9 +262,10 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
}
|
||||
|
||||
void _openFile(_FileEntry entry) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => MdViewerPage(filePath: entry.path),
|
||||
));
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MdViewerPage(filePath: entry.path),
|
||||
));
|
||||
}
|
||||
|
||||
void _showSettingsSheet() {
|
||||
@@ -249,6 +276,7 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
return StatefulBuilder(
|
||||
builder: (ctx, setLocalState) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
@@ -256,24 +284,31 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 36, height: 4,
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFDDDDDD),
|
||||
color: colors.onSurface.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Align(
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text('目录显示设置', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
child: Text('目录显示设置',
|
||||
style: TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('显示空目录', style: TextStyle(fontSize: 14, color: Color(0xFF333333))),
|
||||
subtitle: const Text('关闭后隐藏无 Markdown 文件的目录', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
title: Text('显示空目录',
|
||||
style: TextStyle(
|
||||
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75))),
|
||||
subtitle: Text('关闭后隐藏无 Markdown 文件的目录',
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
value: _showEmptyDirs,
|
||||
activeColor: const Color(0xFF1A1A1A),
|
||||
activeTrackColor: colors.primary,
|
||||
onChanged: (val) {
|
||||
setLocalState(() => _showEmptyDirs = val);
|
||||
UserPrefs().setShowEmptyDirs(val);
|
||||
@@ -281,13 +316,17 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
_loadDirectory();
|
||||
},
|
||||
),
|
||||
const Divider(height: 0.5, color: Color(0xFFF0F0F0)),
|
||||
Divider(height: 0.5, color: colors.outlineVariant),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('显示纯图片目录', style: TextStyle(fontSize: 14, color: Color(0xFF333333))),
|
||||
subtitle: const Text('关闭后隐藏只含图片、无 Markdown 的目录', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
title: Text('显示纯图片目录',
|
||||
style: TextStyle(
|
||||
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75))),
|
||||
subtitle: Text('关闭后隐藏只含图片、无 Markdown 的目录',
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
value: _showImageOnlyDirs,
|
||||
activeColor: const Color(0xFF1A1A1A),
|
||||
activeTrackColor: colors.primary,
|
||||
onChanged: (val) {
|
||||
setLocalState(() => _showImageOnlyDirs = val);
|
||||
UserPrefs().setShowImageOnlyDirs(val);
|
||||
@@ -304,40 +343,26 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
);
|
||||
}
|
||||
|
||||
bool get _canGoBack => _currentPath != null && _rootPath != null && _currentPath != _rootPath;
|
||||
bool get _canGoBack =>
|
||||
_currentPath != null && _rootPath != null && _currentPath != _rootPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
_currentPath != null ? p.basename(_currentPath!) : 'Markdown 阅读',
|
||||
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
||||
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Color(0xFF1A1A1A)),
|
||||
icon: Icon(Icons.arrow_back, color: colors.onSurface),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
actions: [
|
||||
// if (_canGoBack)
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.only(right: 8),
|
||||
// child: GestureDetector(
|
||||
// onTap: _goBack,
|
||||
// child: Container(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
// decoration: BoxDecoration(
|
||||
// color: const Color(0xFFF5F5F5),
|
||||
// borderRadius: BorderRadius.circular(14),
|
||||
// border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
// ),
|
||||
// child: const Text('返回上级', style: TextStyle(fontSize: 12, color: Color(0xFF666666))),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
if (_currentPath != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
@@ -346,86 +371,109 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outline, width: 0.5),
|
||||
),
|
||||
child: const Text('更换目录', style: TextStyle(fontSize: 12, color: Color(0xFF888888))),
|
||||
child: Text('更换目录',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.tune, size: 20, color: Color(0xFF888888)),
|
||||
icon: Icon(Icons.tune, size: 20, color: colors.onSurface.withValues(alpha: 0.5)),
|
||||
onPressed: _showSettingsSheet,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
),
|
||||
body: _buildBody(),
|
||||
body: _buildBody(colors),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
Widget _buildBody(ColorScheme colors) {
|
||||
if (_currentPath == null) {
|
||||
return _buildWelcome();
|
||||
return _buildWelcome(colors);
|
||||
}
|
||||
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator(color: Color(0xFF1A1A1A)));
|
||||
return Center(child: CircularProgressIndicator(color: colors.primary));
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return _buildError();
|
||||
return _buildError(colors);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _loadDirectory,
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: Colors.white,
|
||||
child: _entries.isEmpty ? _buildEmpty() : ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: (_canGoBack ? 1 : 0) + _entries.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFF0F0F0)),
|
||||
itemBuilder: (context, index) {
|
||||
if (_canGoBack && index == 0) {
|
||||
return ListTile(
|
||||
leading: Container(
|
||||
width: 36, height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(Icons.arrow_upward, size: 18, color: Color(0xFF888888)),
|
||||
),
|
||||
title: const Text('..', style: TextStyle(fontSize: 14, color: Color(0xFF888888))),
|
||||
onTap: _goBack,
|
||||
);
|
||||
}
|
||||
final entry = _entries[_canGoBack ? index - 1 : index];
|
||||
return ListTile(
|
||||
leading: Container(
|
||||
width: 36, height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: entry.isDir ? const Color(0xFFF0F7FF) : const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
entry.isDir ? Icons.folder_outlined : Icons.description_outlined,
|
||||
size: 18,
|
||||
color: entry.isDir ? const Color(0xFF4A90D9) : const Color(0xFF666666),
|
||||
),
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.surface,
|
||||
child: _entries.isEmpty
|
||||
? _buildEmpty(colors)
|
||||
: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: (_canGoBack ? 1 : 0) + _entries.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
Divider(height: 0.5, thickness: 0.5, color: colors.outlineVariant),
|
||||
itemBuilder: (context, index) {
|
||||
if (_canGoBack && index == 0) {
|
||||
return ListTile(
|
||||
leading: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(Icons.arrow_upward,
|
||||
size: 18, color: colors.onSurface.withValues(alpha: 0.5)),
|
||||
),
|
||||
title: Text('..',
|
||||
style: TextStyle(
|
||||
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||
onTap: _goBack,
|
||||
);
|
||||
}
|
||||
final entry = _entries[_canGoBack ? index - 1 : index];
|
||||
return ListTile(
|
||||
leading: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: entry.isDir
|
||||
? const Color(0xFFF0F7FF)
|
||||
: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
entry.isDir ? Icons.folder_outlined : Icons.description_outlined,
|
||||
size: 18,
|
||||
color: entry.isDir
|
||||
? const Color(0xFF4A90D9)
|
||||
: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
title: Text(entry.name,
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
subtitle: entry.isDir
|
||||
? null
|
||||
: Text(_formatSize(entry.size),
|
||||
style: TextStyle(
|
||||
fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
trailing: Icon(entry.isDir ? Icons.chevron_right : Icons.open_in_new_outlined,
|
||||
size: 16, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
onTap: () =>
|
||||
entry.isDir ? _enterDirectory(entry.path) : _openFile(entry),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(entry.name, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: entry.isDir ? null : Text(_formatSize(entry.size), style: const TextStyle(fontSize: 11, color: Color(0xFF999999))),
|
||||
trailing: Icon(entry.isDir ? Icons.chevron_right : Icons.open_in_new_outlined, size: 16, color: const Color(0xFFCCCCCC)),
|
||||
onTap: () => entry.isDir ? _enterDirectory(entry.path) : _openFile(entry),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWelcome() {
|
||||
Widget _buildWelcome(ColorScheme colors) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40),
|
||||
@@ -433,27 +481,34 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 80, height: 80,
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(Icons.folder_open_outlined, size: 40, color: Color(0xFFCCCCCC)),
|
||||
child: Icon(Icons.folder_open_outlined,
|
||||
size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text('Markdown 阅读', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text('Markdown 阅读',
|
||||
style: TextStyle(
|
||||
fontSize: 20, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
const SizedBox(height: 8),
|
||||
const Text('选择一个包含 .md 文件的文件夹', style: TextStyle(fontSize: 14, color: Color(0xFF999999))),
|
||||
Text('选择一个包含 .md 文件的文件夹',
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const SizedBox(height: 32),
|
||||
GestureDetector(
|
||||
onTap: _pickDirectory,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: const Text('选择目录', style: TextStyle(fontSize: 15, color: Colors.white, fontWeight: FontWeight.w500)),
|
||||
child: Text('选择目录',
|
||||
style: TextStyle(
|
||||
fontSize: 15, color: colors.onPrimary, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -462,28 +517,31 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmpty() {
|
||||
Widget _buildEmpty(ColorScheme colors) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.folder_open_outlined, size: 64, color: Color(0xFFE0E0E0)),
|
||||
Icon(Icons.folder_open_outlined, size: 64, color: colors.outline),
|
||||
const SizedBox(height: 16),
|
||||
const Text('此目录下没有 Markdown 文件', style: TextStyle(fontSize: 15, color: Color(0xFF999999))),
|
||||
Text('此目录下没有 Markdown 文件',
|
||||
style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const SizedBox(height: 4),
|
||||
Text(_currentPath ?? '', style: const TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))),
|
||||
Text(_currentPath ?? '',
|
||||
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||
const SizedBox(height: 24),
|
||||
GestureDetector(
|
||||
onTap: _canGoBack ? _goBack : () => _pickDirectory(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFDDDDDD)),
|
||||
border: Border.all(color: colors.onSurface.withValues(alpha: 0.15)),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
_canGoBack ? '返回上级目录' : '换一个目录',
|
||||
style: const TextStyle(fontSize: 13, color: Color(0xFF888888)),
|
||||
style: TextStyle(
|
||||
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -492,18 +550,22 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildError() {
|
||||
Widget _buildError(ColorScheme colors) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48, color: Color(0xFFCCCCCC)),
|
||||
Icon(Icons.error_outline,
|
||||
size: 48, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
const SizedBox(height: 16),
|
||||
Text(_error!, style: const TextStyle(fontSize: 14, color: Color(0xFF999999)), textAlign: TextAlign.center),
|
||||
Text(_error!,
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
textAlign: TextAlign.center),
|
||||
const SizedBox(height: 8),
|
||||
Text('路径: ${_currentPath ?? ""}', style: const TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))),
|
||||
Text('路径: ${_currentPath ?? ""}',
|
||||
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -513,10 +575,11 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Text('重试', style: TextStyle(fontSize: 13, color: Colors.white)),
|
||||
child: Text('重试',
|
||||
style: TextStyle(fontSize: 13, color: colors.onPrimary)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -525,10 +588,12 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFDDDDDD)),
|
||||
border: Border.all(color: colors.onSurface.withValues(alpha: 0.15)),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Text('更换目录', style: TextStyle(fontSize: 13, color: Color(0xFF888888))),
|
||||
child: Text('更换目录',
|
||||
style: TextStyle(
|
||||
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -50,10 +50,11 @@ class _MdViewerPageState extends State<MdViewerPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final fileName = widget.filePath.split('/').last;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
@@ -61,69 +62,71 @@ class _MdViewerPageState extends State<MdViewerPage> {
|
||||
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
body: _buildBody(),
|
||||
body: _buildBody(colors),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
Widget _buildBody(ColorScheme colors) {
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator(color: Color(0xFF1A1A1A)));
|
||||
return Center(child: CircularProgressIndicator(color: colors.primary));
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return _buildErrorState();
|
||||
return _buildErrorState(colors);
|
||||
}
|
||||
|
||||
return Markdown(
|
||||
data: _content,
|
||||
padding: const EdgeInsets.all(20),
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
h1: const TextStyle(
|
||||
h1: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.4,
|
||||
),
|
||||
h2: const TextStyle(
|
||||
h2: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.4,
|
||||
),
|
||||
h3: const TextStyle(
|
||||
h3: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.4,
|
||||
),
|
||||
p: const TextStyle(
|
||||
p: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.8,
|
||||
),
|
||||
code: const TextStyle(
|
||||
code: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1A1A1A),
|
||||
backgroundColor: Color(0xFFF5F5F5),
|
||||
color: colors.onSurface,
|
||||
backgroundColor: colors.surfaceContainerHighest,
|
||||
),
|
||||
codeblockDecoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
color: colors.surfaceContainerHighest,
|
||||
border: Border.all(color: colors.outline),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
codeblockPadding: const EdgeInsets.all(12),
|
||||
blockquote: const TextStyle(
|
||||
blockquote: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
blockquoteDecoration: const BoxDecoration(
|
||||
border: Border(left: BorderSide(color: Color(0xFF999999), width: 4)),
|
||||
blockquoteDecoration: BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: colors.onSurface.withValues(alpha: 0.4), width: 4)),
|
||||
),
|
||||
blockquotePadding: const EdgeInsets.only(left: 12),
|
||||
listBullet: const TextStyle(
|
||||
listBullet: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
listIndent: 24,
|
||||
a: const TextStyle(
|
||||
@@ -133,12 +136,12 @@ class _MdViewerPageState extends State<MdViewerPage> {
|
||||
),
|
||||
),
|
||||
// ignore: deprecated_member_use
|
||||
imageBuilder: (uri, title, alt) => _buildImage(uri.toString(), alt),
|
||||
imageBuilder: (uri, title, alt) => _buildImage(colors, uri.toString(), alt),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建图片显示
|
||||
Widget _buildImage(String uri, String? alt) {
|
||||
Widget _buildImage(ColorScheme colors, String uri, String? alt) {
|
||||
if (uri.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
// 处理相对路径:基于 md 文件所在目录
|
||||
@@ -158,17 +161,19 @@ class _MdViewerPageState extends State<MdViewerPage> {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.broken_image_outlined, size: 20, color: Color(0xFF999999)),
|
||||
Icon(Icons.broken_image_outlined,
|
||||
size: 20, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
alt ?? '图片加载失败',
|
||||
style: const TextStyle(fontSize: 13, color: Color(0xFF999999)),
|
||||
style:
|
||||
TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -179,16 +184,17 @@ class _MdViewerPageState extends State<MdViewerPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState() {
|
||||
Widget _buildErrorState(ColorScheme colors) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48, color: Color(0xFFCCCCCC)),
|
||||
Icon(Icons.error_outline,
|
||||
size: 48, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_error!,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF999999)),
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -5,9 +5,9 @@ import 'package:webview_flutter/webview_flutter.dart';
|
||||
/// 豆瓣影视WebView页面 - 用于抓取影视信息
|
||||
class DoubanWebViewPage extends StatefulWidget {
|
||||
final String url;
|
||||
|
||||
|
||||
const DoubanWebViewPage({super.key, required this.url});
|
||||
|
||||
|
||||
@override
|
||||
State<DoubanWebViewPage> createState() => _DoubanWebViewPageState();
|
||||
}
|
||||
@@ -16,21 +16,21 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
late WebViewController _controller;
|
||||
bool _isLoading = true;
|
||||
bool _canExtract = false;
|
||||
bool _isExtracting = false; // 防止重复提取
|
||||
|
||||
bool _isExtracting = false; // 防止重复提取
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initWebView();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 清理 WebView 资源
|
||||
_controller.loadRequest(Uri.parse('about:blank'));
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
void _initWebView() {
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
@@ -55,23 +55,26 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
)
|
||||
..loadRequest(Uri.parse(widget.url));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('豆瓣影视'),
|
||||
leading: _buildBackButton(),
|
||||
actions: [
|
||||
// 提取按钮 - 始终显示
|
||||
_buildActionButton(
|
||||
colors: colors,
|
||||
icon: Icons.auto_fix_high_outlined,
|
||||
onPressed: _showExtractedInfo,
|
||||
tooltip: '提取信息',
|
||||
),
|
||||
// 刷新按钮
|
||||
_buildActionButton(
|
||||
colors: colors,
|
||||
icon: Icons.refresh,
|
||||
onPressed: () => _controller.reload(),
|
||||
tooltip: '刷新',
|
||||
@@ -83,16 +86,14 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
children: [
|
||||
WebViewWidget(controller: _controller),
|
||||
// 加载指示器
|
||||
if (_isLoading)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
if (_isLoading) const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// 构建返回按钮
|
||||
Widget _buildBackButton() {
|
||||
return Container(
|
||||
@@ -121,6 +122,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
|
||||
/// 构建右上角操作按钮
|
||||
Widget _buildActionButton({
|
||||
required ColorScheme colors,
|
||||
required IconData icon,
|
||||
required VoidCallback onPressed,
|
||||
required String tooltip,
|
||||
@@ -138,75 +140,86 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(icon, color: const Color(0xFF1A1A1A), size: 22),
|
||||
child: Icon(icon, color: colors.onSurface, size: 22),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// 显示提取的信息对话框
|
||||
Future<void> _showExtractedInfo() async {
|
||||
// 先提取信息
|
||||
final movieInfo = await _extractMovieInfo();
|
||||
if (movieInfo == null) return;
|
||||
|
||||
|
||||
// 显示提取的信息
|
||||
if (mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text(
|
||||
'提取的影视信息',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
),
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow('标题', movieInfo['title']?.toString() ?? '未提取到'),
|
||||
_buildInfoRow('导演', movieInfo['director']?.toString() ?? '未提取到'),
|
||||
_buildInfoRow('类型', movieInfo['genres']?.toString() ?? '未提取到'),
|
||||
_buildInfoRow('上映日期', movieInfo['releaseDate']?.toString() ?? '未提取到'),
|
||||
if (movieInfo['summary'] != null)
|
||||
_buildInfoRow('简介', movieInfo['summary'].toString().substring(0,
|
||||
movieInfo['summary'].toString().length > 100 ? 100 : movieInfo['summary'].toString().length) + '...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Color(0xFF999999)),
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: Text(
|
||||
'提取的影视信息',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context, movieInfo);
|
||||
},
|
||||
child: const Text(
|
||||
'使用此信息',
|
||||
style: TextStyle(color: Color(0xFF1A1A1A), fontWeight: FontWeight.w600),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(colors, '标题', movieInfo['title']?.toString() ?? '未提取到'),
|
||||
_buildInfoRow(colors, '导演', movieInfo['director']?.toString() ?? '未提取到'),
|
||||
_buildInfoRow(colors, '类型', movieInfo['genres']?.toString() ?? '未提取到'),
|
||||
_buildInfoRow(colors, '上映日期', movieInfo['releaseDate']?.toString() ?? '未提取到'),
|
||||
if (movieInfo['summary'] != null)
|
||||
_buildInfoRow(
|
||||
colors,
|
||||
'简介',
|
||||
movieInfo['summary'].toString().substring(
|
||||
0,
|
||||
movieInfo['summary'].toString().length > 100
|
||||
? 100
|
||||
: movieInfo['summary'].toString().length) +
|
||||
'...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
Navigator.pop(context, movieInfo);
|
||||
},
|
||||
child: Text(
|
||||
'使用此信息',
|
||||
style: TextStyle(
|
||||
color: colors.onSurface, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 构建信息行
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
Widget _buildInfoRow(ColorScheme colors, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
@@ -216,18 +229,18 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
width: 64,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -240,10 +253,10 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
Future<Map<String, dynamic>?> _extractMovieInfo() async {
|
||||
// 检查是否已提取过,避免重复点击
|
||||
if (_isExtracting) return null;
|
||||
|
||||
|
||||
try {
|
||||
_isExtracting = true;
|
||||
|
||||
|
||||
// 显示加载提示
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -252,16 +265,16 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
// 执行JavaScript代码提取页面信息
|
||||
final result = await _controller.runJavaScriptReturningResult(r'''
|
||||
(function() {
|
||||
const info = {};
|
||||
|
||||
|
||||
// 获取标题 - 移动版页面
|
||||
const titleEl = document.querySelector('.sub-title');
|
||||
info.title = titleEl ? titleEl.textContent.trim() : '';
|
||||
|
||||
|
||||
// 获取年份 - 从 original-title 中提取
|
||||
const originalTitleEl = document.querySelector('.sub-original-title');
|
||||
if (originalTitleEl) {
|
||||
@@ -270,7 +283,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
} else {
|
||||
info.year = '';
|
||||
}
|
||||
|
||||
|
||||
// 获取封面图 - 从 sub-cover 中的 img 标签获取
|
||||
const coverEl = document.querySelector('.sub-cover img');
|
||||
if (coverEl) {
|
||||
@@ -283,11 +296,11 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
} else {
|
||||
info.coverUrl = '';
|
||||
}
|
||||
|
||||
|
||||
// 获取评分 - 移动版可能在 mark-item 中
|
||||
const ratingEl = document.querySelector('.rating-num') || document.querySelector('.score');
|
||||
info.rating = ratingEl ? ratingEl.textContent.trim() : '';
|
||||
|
||||
|
||||
// 获取导演 - 从演职员列表中找
|
||||
const directorEl = document.querySelector('.movie-celebrities .item__celebrity .role');
|
||||
if (directorEl && directorEl.textContent.includes('导演')) {
|
||||
@@ -296,7 +309,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
} else {
|
||||
info.director = '';
|
||||
}
|
||||
|
||||
|
||||
// 获取编剧 - 从演职员列表中找(匹配"编剧"或"剧本")
|
||||
const writerEls = document.querySelectorAll('.movie-celebrities .item__celebrity');
|
||||
const writers = [];
|
||||
@@ -308,7 +321,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
}
|
||||
});
|
||||
info.writers = writers;
|
||||
|
||||
|
||||
// 获取主演- 从演职员列表中找前5个
|
||||
const actorEls = document.querySelectorAll('.movie-celebrities .item__celebrity');
|
||||
const actors = [];
|
||||
@@ -316,10 +329,10 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
const roleEl = el
|
||||
.querySelector('.role');
|
||||
if (roleEl && (
|
||||
roleEl.textContent.includes('配音') ||
|
||||
roleEl.textContent.includes('主演') ||
|
||||
roleEl.textContent.includes('配音') ||
|
||||
roleEl.textContent.includes('主演') ||
|
||||
roleEl.textContent.includes('演员') ||
|
||||
roleEl.textContent.includes('参演') ||
|
||||
roleEl.textContent.includes('参演') ||
|
||||
roleEl.textContent.includes('饰')
|
||||
)) {
|
||||
const nameEl = el.querySelector('.name');
|
||||
@@ -327,20 +340,20 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
}
|
||||
});
|
||||
info.actors = actors;
|
||||
|
||||
|
||||
// 获取类型 - 从 sub-meta 或标签中提取
|
||||
const metaEl = document.querySelector('.sub-meta');
|
||||
if (metaEl) {
|
||||
const metaText = metaEl.textContent;
|
||||
const parts = metaText.split('/').map(s => s.trim());
|
||||
// 过滤出类型(通常是中文,不是日期,不是时长)
|
||||
info.genres = parts.filter(p =>
|
||||
info.genres = parts.filter(p =>
|
||||
p && !p.match(/^\d{4}/) && !p.includes('分钟') && !p.includes('上映')
|
||||
).join(',');
|
||||
} else {
|
||||
info.genres = '';
|
||||
}
|
||||
|
||||
|
||||
// 获取上映日期
|
||||
if (metaEl) {
|
||||
const dateMatch = metaEl.textContent.match(/(\d{4}-\d{2}-\d{2})/);
|
||||
@@ -348,7 +361,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
} else {
|
||||
info.releaseDate = '';
|
||||
}
|
||||
|
||||
|
||||
// 获取简介
|
||||
const summaryEl = document.querySelector('.subject-intro p');
|
||||
if (summaryEl) {
|
||||
@@ -356,7 +369,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
} else {
|
||||
info.summary = '';
|
||||
}
|
||||
|
||||
|
||||
// 获取别名 - 从 original-title 中提取(去掉年份)
|
||||
if (originalTitleEl) {
|
||||
const fullText = originalTitleEl.textContent.trim();
|
||||
@@ -364,14 +377,14 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
} else {
|
||||
info.alternateTitles = [];
|
||||
}
|
||||
|
||||
|
||||
return JSON.stringify(info);
|
||||
})()
|
||||
''');
|
||||
|
||||
|
||||
// 关闭加载提示
|
||||
if (mounted) Navigator.pop(context);
|
||||
|
||||
|
||||
// 解析提取的信息
|
||||
// result 是 JavaScript 执行结果,已经是 JSON 字符串(带引号的)
|
||||
final String jsonStr = result.toString();
|
||||
@@ -380,12 +393,12 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
||||
? jsonDecode(jsonStr) as String
|
||||
: jsonStr;
|
||||
final Map<String, dynamic> movieInfo = jsonDecode(cleanJson) as Map<String, dynamic>;
|
||||
|
||||
|
||||
return movieInfo;
|
||||
} catch (e) {
|
||||
// 关闭加载提示
|
||||
if (mounted) Navigator.pop(context);
|
||||
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('提取信息失败: $e')),
|
||||
|
||||
@@ -16,9 +16,9 @@ import 'movie_share_page.dart';
|
||||
/// 影视详情页 - 极简主义设计
|
||||
class MovieDetailPage extends StatefulWidget {
|
||||
final Movie movie;
|
||||
|
||||
|
||||
const MovieDetailPage({super.key, required this.movie});
|
||||
|
||||
|
||||
@override
|
||||
State<MovieDetailPage> createState() => _MovieDetailPageState();
|
||||
}
|
||||
@@ -27,77 +27,53 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
// 页面获得焦点时刷新数据
|
||||
_refreshMovieData();
|
||||
}
|
||||
|
||||
|
||||
void _refreshMovieData() {
|
||||
final provider = context.read<AppProvider>();
|
||||
// 强制刷新当前影视数据
|
||||
provider.loadMovies();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 从 Provider 获取最新的 movie 数据,实现动态刷新
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final movie = context.watch<AppProvider>().movies
|
||||
.where((m) => m.id == widget.movie.id)
|
||||
.firstOrNull ?? widget.movie;
|
||||
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
body: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
// 顶部海报区域
|
||||
_buildSliverAppBar(movie),
|
||||
|
||||
// 内容区域
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 基本信息
|
||||
_buildBasicInfo(movie),
|
||||
|
||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
||||
|
||||
// 导演
|
||||
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||
if (movie.directors.isNotEmpty)
|
||||
_buildDirectorsSection(movie),
|
||||
|
||||
// 编剧
|
||||
if (movie.writers.isNotEmpty)
|
||||
_buildWritersSection(movie),
|
||||
|
||||
// 主演
|
||||
if (movie.actors.isNotEmpty)
|
||||
_buildActorsSection(movie),
|
||||
|
||||
// 类型
|
||||
if (movie.genres.isNotEmpty)
|
||||
_buildGenresSection(movie),
|
||||
|
||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
||||
|
||||
// 简介
|
||||
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||
if (movie.summary != null && movie.summary!.isNotEmpty)
|
||||
_buildSummarySection(movie),
|
||||
|
||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
||||
|
||||
// 影评和海报墙入口
|
||||
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||
_buildExtraSections(movie),
|
||||
|
||||
const SizedBox(height: 120),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 右下角悬浮按钮组
|
||||
Positioned(
|
||||
right: 16,
|
||||
bottom: 24,
|
||||
@@ -107,9 +83,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建右下角悬浮按钮组
|
||||
|
||||
Widget _buildFloatingActionButtons(Movie movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -117,13 +93,16 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
icon: Icons.edit_outlined,
|
||||
onPressed: () => _navigateToEdit(context),
|
||||
tooltip: '编辑',
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildFloatingButton(
|
||||
icon: Icons.delete_outline,
|
||||
onPressed: () => _showDeleteDialog(context),
|
||||
tooltip: '删除',
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: colors.error,
|
||||
foregroundColor: colors.onError,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildFloatingButton(
|
||||
@@ -131,17 +110,18 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
onPressed: () => _showSharePoster(movie),
|
||||
tooltip: '分享海报',
|
||||
backgroundColor: const Color(0xFF4CAF50),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建单个悬浮按钮
|
||||
Widget _buildFloatingButton({
|
||||
required IconData icon,
|
||||
required VoidCallback onPressed,
|
||||
required String tooltip,
|
||||
Color backgroundColor = const Color(0xFF1A1A1A),
|
||||
required Color backgroundColor,
|
||||
required Color foregroundColor,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
@@ -160,15 +140,15 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
],
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(icon, size: 18, color: Colors.white),
|
||||
icon: Icon(icon, size: 18, color: foregroundColor),
|
||||
onPressed: onPressed,
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: tooltip,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建带背景的返回按钮
|
||||
Widget _buildBackButton() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||
@@ -194,21 +174,19 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建顶部 AppBar
|
||||
Widget _buildSliverAppBar(Movie movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return SliverAppBar(
|
||||
expandedHeight: 320,
|
||||
pinned: true,
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
backgroundColor: colors.surfaceContainerHighest,
|
||||
leading: _buildBackButton(),
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: _buildPosterSection(movie),
|
||||
),
|
||||
// 右上角按钮已移到右下角悬浮按钮
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建海报区域
|
||||
|
||||
Widget _buildPosterSection(Movie movie) {
|
||||
return SizedBox.expand(
|
||||
child: movie.posterPath != null && movie.posterPath!.isNotEmpty
|
||||
@@ -220,79 +198,74 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
: _buildPosterPlaceholder(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildPosterPlaceholder() {
|
||||
return const Center(
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.movie_outlined,
|
||||
size: 64,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'暂无海报',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建基本信息
|
||||
|
||||
Widget _buildBasicInfo(Movie movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 影视名称
|
||||
Text(
|
||||
movie.title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
|
||||
// 别名(显示在主名称下面,用 / 分隔)
|
||||
if (movie.alternateTitles.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
movie.alternateTitles.join(' / '),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 评分和状态
|
||||
Row(
|
||||
children: [
|
||||
if (movie.rating != null) ...[
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.star,
|
||||
size: 20,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
movie.rating!.toStringAsFixed(1),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@@ -300,62 +273,56 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
_buildStatusTag(movie),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 上映日期
|
||||
if (movie.releaseDate != null)
|
||||
Text(
|
||||
'${movie.releaseDate!.year}年${movie.releaseDate!.month.toString().padLeft(2, '0')}月上映',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 观看日期
|
||||
if (movie.watchDate != null)
|
||||
Text(
|
||||
'观看于 ${_formatDate(movie.watchDate!)}',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建状态标签
|
||||
|
||||
Widget _buildStatusTag(Movie movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
String label;
|
||||
Color bgColor;
|
||||
Color textColor;
|
||||
switch (movie.status) {
|
||||
case 'watched':
|
||||
label = '已看';
|
||||
bgColor = const Color(0xFF1A1A1A);
|
||||
textColor = Colors.white;
|
||||
bgColor = colors.primary;
|
||||
textColor = colors.onPrimary;
|
||||
break;
|
||||
case 'watching':
|
||||
label = '在看';
|
||||
bgColor = const Color(0xFFF0F0F0);
|
||||
textColor = const Color(0xFF666666);
|
||||
bgColor = colors.outlineVariant;
|
||||
textColor = colors.onSurface.withValues(alpha: 0.6);
|
||||
break;
|
||||
case 'want_to_watch':
|
||||
label = '想看';
|
||||
bgColor = const Color(0xFFF5F5F5);
|
||||
textColor = const Color(0xFF999999);
|
||||
bgColor = colors.surfaceContainerHighest;
|
||||
textColor = colors.onSurface.withValues(alpha: 0.4);
|
||||
break;
|
||||
default:
|
||||
label = '未知';
|
||||
bgColor = const Color(0xFFEEEEEE);
|
||||
textColor = const Color(0xFFCCCCCC);
|
||||
bgColor = colors.outlineVariant;
|
||||
textColor = colors.onSurface.withValues(alpha: 0.25);
|
||||
}
|
||||
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
@@ -372,30 +339,30 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建导演区域
|
||||
|
||||
Widget _buildDirectorsSection(Movie movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Text(
|
||||
'导演',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
movie.directors.join(','),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -405,29 +372,29 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建编剧区域
|
||||
Widget _buildWritersSection(Movie movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Text(
|
||||
'编剧',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
movie.writers.join(','),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -437,29 +404,29 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建主演区域
|
||||
Widget _buildActorsSection(Movie movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Text(
|
||||
'主演',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
movie.actors.join(','),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -469,20 +436,20 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建类型区域
|
||||
Widget _buildGenresSection(Movie movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Text(
|
||||
'类型',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -494,14 +461,14 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
genre,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -512,9 +479,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建简介区域
|
||||
|
||||
Widget _buildSummarySection(Movie movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
@@ -526,17 +493,17 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
width: 4,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
Text(
|
||||
'简介',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -545,14 +512,14 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
movie.summary!,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.8,
|
||||
),
|
||||
),
|
||||
@@ -561,9 +528,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建额外功能区域(影评、海报墙)
|
||||
|
||||
Widget _buildExtraSections(Movie movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
@@ -575,23 +542,22 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
width: 4,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
Text(
|
||||
'更多',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 影评入口
|
||||
_buildExtraSectionItem(
|
||||
icon: Icons.rate_review_outlined,
|
||||
title: '影评',
|
||||
@@ -601,7 +567,6 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
onTap: () => _navigateToReviews(movie),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 海报墙入口
|
||||
_buildExtraSectionItem(
|
||||
icon: Icons.photo_library_outlined,
|
||||
title: '海报墙',
|
||||
@@ -615,7 +580,6 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建更多区域项
|
||||
Widget _buildExtraSectionItem({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
@@ -624,14 +588,15 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
required String unit,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -639,14 +604,14 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: const Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -656,10 +621,10 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -669,9 +634,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
final count = snapshot.data ?? 0;
|
||||
return Text(
|
||||
count > 0 ? '$count $unit' : emptyText,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -679,16 +644,16 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _navigateToReviews(Movie movie) {
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -697,7 +662,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _navigateToPosters(Movie movie) {
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -706,39 +671,38 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式化日期
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 跳转到编辑页面
|
||||
|
||||
void _navigateToEdit(BuildContext context) {
|
||||
Navigator.pushNamed(context, '/movie-form', arguments: widget.movie).then((_) {
|
||||
context.read<AppProvider>().loadMovies();
|
||||
});
|
||||
}
|
||||
|
||||
/// 显示删除对话框
|
||||
|
||||
void _showDeleteDialog(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text(
|
||||
title: Text(
|
||||
'确认删除',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
content: Text(
|
||||
'确定要删除"${widget.movie.title}"吗?删除后可在回收站恢复。',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -746,7 +710,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF666666),
|
||||
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
child: const Text('取消'),
|
||||
@@ -760,8 +724,8 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
ToastUtil.show(context, '已删除');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colors.error,
|
||||
foregroundColor: colors.onError,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -775,18 +739,14 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 请求存储权限
|
||||
|
||||
Future<bool> _requestStoragePermission() async {
|
||||
// Android 13+ 使用新的权限
|
||||
if (Platform.isAndroid) {
|
||||
final sdkInt = await _getAndroidSdkInt();
|
||||
if (sdkInt >= 33) {
|
||||
// Android 13+ 使用 READ_MEDIA_IMAGES
|
||||
final status = await Permission.photos.request();
|
||||
return status.isGranted;
|
||||
} else {
|
||||
// Android 12 及以下使用存储权限
|
||||
var status = await Permission.storage.request();
|
||||
if (status.isDenied) {
|
||||
status = await Permission.storage.request();
|
||||
@@ -794,18 +754,13 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
return status.isGranted;
|
||||
}
|
||||
}
|
||||
// iOS 不需要额外权限来保存到应用沙盒
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 获取 Android SDK 版本
|
||||
|
||||
Future<int> _getAndroidSdkInt() async {
|
||||
// 简化处理,实际可以通过 platform channel 获取
|
||||
// 这里默认返回较低版本,使用传统存储权限
|
||||
return 30;
|
||||
}
|
||||
|
||||
/// 显示分享海报页面
|
||||
void _showSharePoster(Movie movie) {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,8 +45,9 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('海报墙'),
|
||||
actions: [
|
||||
@@ -66,6 +67,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -74,21 +76,21 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.photo_library_outlined,
|
||||
size: 40,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
Text(
|
||||
'暂无海报',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -97,15 +99,15 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'添加记录',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
color: colors.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -130,10 +132,11 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
}
|
||||
|
||||
Widget _buildPosterItem(MoviePoster poster, int index) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
// 根据索引生成不同的高度,实现瀑布流效果
|
||||
final heights = [180.0, 220.0, 160.0, 200.0, 240.0, 190.0];
|
||||
final height = heights[index % heights.length];
|
||||
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _showPosterDetail(poster),
|
||||
onLongPress: () => _showDeleteDialog(poster),
|
||||
@@ -158,10 +161,10 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
Image.file(
|
||||
File(poster.posterPath),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const Center(
|
||||
errorBuilder: (_, __, ___) => Center(
|
||||
child: Icon(
|
||||
Icons.broken_image,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -194,7 +197,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
void _showPosterDetail(MoviePoster poster) {
|
||||
// 找到当前海报的索引
|
||||
final initialIndex = _posters.indexWhere((p) => p.id == poster.id);
|
||||
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -210,61 +213,72 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
// 显示选择对话框
|
||||
final result = await showModalBottomSheet<int>(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) => SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 顶部指示条
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE0E0E0),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 标题
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'添加海报',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 从相册选择
|
||||
_buildAddOption(
|
||||
icon: Icons.photo_library_outlined,
|
||||
title: '从相册选择',
|
||||
subtitle: '选择本地图片',
|
||||
onTap: () => Navigator.pop(context, 0),
|
||||
),
|
||||
// 网络链接
|
||||
_buildAddOption(
|
||||
icon: Icons.link_outlined,
|
||||
title: '网络链接',
|
||||
subtitle: '输入图片URL地址',
|
||||
onTap: () => Navigator.pop(context, 1),
|
||||
),
|
||||
],
|
||||
builder: (context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 顶部指示条
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.outline,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 标题
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'添加海报',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 从相册选择
|
||||
_buildAddOption(
|
||||
colors: colors,
|
||||
icon: Icons.photo_library_outlined,
|
||||
title: '从相册选择',
|
||||
subtitle: '选择本地图片',
|
||||
onTap: () => Navigator.pop(context, 0),
|
||||
),
|
||||
// 网络链接
|
||||
_buildAddOption(
|
||||
colors: colors,
|
||||
icon: Icons.link_outlined,
|
||||
title: '网络链接',
|
||||
subtitle: '输入图片URL地址',
|
||||
onTap: () => Navigator.pop(context, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result == null) return;
|
||||
@@ -289,10 +303,10 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
if (pickedFile != null) {
|
||||
// 生成文件名
|
||||
final fileName = 'posterimg_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
|
||||
|
||||
// 保存到 posterimgs 子目录: images/movies/{movieId}/posterimgs/{fileName}
|
||||
final targetPath = await ImagePathHelper.instance.getMoviePosterImgPath(
|
||||
widget.movie.id,
|
||||
widget.movie.id,
|
||||
fileName
|
||||
);
|
||||
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
|
||||
@@ -323,55 +337,58 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
/// 从网络链接添加
|
||||
Future<void> _pickFromUrl() async {
|
||||
final urlController = TextEditingController();
|
||||
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('添加网络图片'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'请输入图片链接地址',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF666666),
|
||||
builder: (context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('添加网络图片'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'请输入图片链接地址',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: urlController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'https://example.com/image.jpg',
|
||||
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
border: const UnderlineInputBorder(),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: colors.outline),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: colors.primary),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: urlController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'https://example.com/image.jpg',
|
||||
hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
|
||||
border: UnderlineInputBorder(),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Color(0xFFE5E5E5)),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Color(0xFF1A1A1A)),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
keyboardType: TextInputType.url,
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text('确定', style: TextStyle(color: colors.onSurface)),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('确定', style: TextStyle(color: Color(0xFF1A1A1A))),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
@@ -404,7 +421,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
'Referer': Uri.parse(url).replace(path: '/').toString(),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('下载失败: HTTP ${response.statusCode}');
|
||||
}
|
||||
@@ -422,10 +439,10 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
|
||||
// 生成文件名
|
||||
final fileName = 'posterimg_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
|
||||
|
||||
// 保存到 posterimgs 子目录
|
||||
final targetPath = await ImagePathHelper.instance.getMoviePosterImgPath(
|
||||
widget.movie.id,
|
||||
widget.movie.id,
|
||||
fileName
|
||||
);
|
||||
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
|
||||
@@ -453,6 +470,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
|
||||
/// 构建添加选项
|
||||
Widget _buildAddOption({
|
||||
required ColorScheme colors,
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
@@ -468,13 +486,13 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 22,
|
||||
color: const Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@@ -484,26 +502,26 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
@@ -515,28 +533,31 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
void _showDeleteDialog(MoviePoster poster) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除这张海报吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeMoviePoster(poster.id);
|
||||
Navigator.pop(context);
|
||||
_loadPosters();
|
||||
ToastUtil.show(context, '已删除');
|
||||
},
|
||||
child: const Text('删除', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除这张海报吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeMoviePoster(poster.id);
|
||||
Navigator.pop(context);
|
||||
_loadPosters();
|
||||
ToastUtil.show(context, '已删除');
|
||||
},
|
||||
child: Text('删除', style: TextStyle(color: colors.error)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,9 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('影评详情'),
|
||||
actions: [
|
||||
@@ -70,9 +71,9 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
||||
// 影评内容
|
||||
Text(
|
||||
_review.content,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.8,
|
||||
),
|
||||
),
|
||||
@@ -82,7 +83,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
||||
// 分隔线
|
||||
Container(
|
||||
height: 0.5,
|
||||
color: const Color(0xFFE5E5E5),
|
||||
color: colors.outline,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
@@ -92,6 +93,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
||||
icon: Icons.person_outline,
|
||||
label: '影评人:',
|
||||
value: _review.reviewer.isNotEmpty ? _review.reviewer : '匿名',
|
||||
colors: colors,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
@@ -102,6 +104,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
||||
icon: Icons.source_outlined,
|
||||
label: '来源:',
|
||||
value: _review.source,
|
||||
colors: colors,
|
||||
),
|
||||
|
||||
if (_review.source.isNotEmpty) const SizedBox(height: 16),
|
||||
@@ -111,6 +114,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
||||
icon: Icons.category_outlined,
|
||||
label: '类型:',
|
||||
value: _review.typeText,
|
||||
colors: colors,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
@@ -120,6 +124,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
||||
icon: Icons.access_time,
|
||||
label: '时间:',
|
||||
value: _formatDate(_review.createdAt),
|
||||
colors: colors,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -132,13 +137,14 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
required ColorScheme colors,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: const Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 固定宽度容器,以"影评人:"的最大宽度为准
|
||||
@@ -146,18 +152,18 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
||||
width: 64,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -46,10 +46,11 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final isEdit = widget.review != null;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: Text(isEdit ? '编辑影评' : '写影评'),
|
||||
actions: [
|
||||
@@ -73,24 +74,24 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
||||
// 顶部信息栏
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
|
||||
bottom: BorderSide(color: colors.outline, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 类型选择
|
||||
_buildTypeSelector(),
|
||||
_buildTypeSelector(colors),
|
||||
const SizedBox(width: 16),
|
||||
// 评论人
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _reviewerController,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
||||
decoration: const InputDecoration(
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: '评论人',
|
||||
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
|
||||
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@@ -103,10 +104,10 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
||||
width: 100,
|
||||
child: TextField(
|
||||
controller: _sourceController,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
||||
decoration: const InputDecoration(
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: '来源',
|
||||
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
|
||||
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@@ -116,7 +117,7 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// 评论内容区域
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
@@ -124,19 +125,19 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.7,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
hintText: '写下你的影评...',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.all(16),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
@@ -153,29 +154,29 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
||||
}
|
||||
|
||||
/// 构建类型选择器
|
||||
Widget _buildTypeSelector() {
|
||||
Widget _buildTypeSelector(ColorScheme colors) {
|
||||
return GestureDetector(
|
||||
onTap: () => _showTypeSelector(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
border: Border.all(color: colors.outline),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_reviewType == 1 ? '短评' : '长评',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.arrow_drop_down,
|
||||
size: 16,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -187,36 +188,42 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
||||
void _showTypeSelector() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
builder: (context) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('短评'),
|
||||
trailing: _reviewType == 1
|
||||
? const Icon(Icons.check, color: Color(0xFF1A1A1A))
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() => _reviewType = 1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
builder: (context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
color: colors.surface,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('短评'),
|
||||
trailing: _reviewType == 1
|
||||
? Icon(Icons.check, color: colors.onSurface)
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() => _reviewType = 1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
Divider(height: 0.5, color: colors.outline),
|
||||
ListTile(
|
||||
title: const Text('长评'),
|
||||
trailing: _reviewType == 2
|
||||
? Icon(Icons.check, color: colors.onSurface)
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() => _reviewType = 2);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 0.5),
|
||||
ListTile(
|
||||
title: const Text('长评'),
|
||||
trailing: _reviewType == 2
|
||||
? const Icon(Icons.check, color: Color(0xFF1A1A1A))
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() => _reviewType = 2);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,19 +73,20 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: _isSearching
|
||||
? TextField(
|
||||
controller: _searchController,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索影评内容、评论人、来源...',
|
||||
hintStyle: TextStyle(color: Color(0xFF999999)),
|
||||
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: const TextStyle(color: Color(0xFF1A1A1A)),
|
||||
style: TextStyle(color: colors.onSurface),
|
||||
onChanged: _onSearchChanged,
|
||||
)
|
||||
: const Text('影评'),
|
||||
@@ -112,6 +113,7 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -120,21 +122,21 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.rate_review_outlined,
|
||||
size: 40,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
Text(
|
||||
'暂无影评',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -143,15 +145,15 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'添加记录',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
color: colors.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -176,13 +178,14 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
||||
}
|
||||
|
||||
Widget _buildReviewCard(MovieReview review) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () => _navigateToReviewDetail(review),
|
||||
onLongPress: () => _showDeleteDialog(review),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
@@ -193,8 +196,8 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: review.reviewType == 1
|
||||
? Colors.white
|
||||
: const Color(0xFF1A1A1A),
|
||||
? colors.surface
|
||||
: colors.primary,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
@@ -202,8 +205,8 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: review.reviewType == 1
|
||||
? const Color(0xFF666666)
|
||||
: Colors.white,
|
||||
? colors.onSurface.withValues(alpha: 0.6)
|
||||
: colors.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -215,9 +218,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
||||
review.content,
|
||||
maxLines: review.reviewType == 1 ? 4 : 8,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -232,9 +235,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
review.reviewer,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -252,9 +255,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
review.source,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -262,9 +265,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
||||
// 日期
|
||||
Text(
|
||||
_formatDate(review.createdAt),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -315,28 +318,31 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
||||
void _showDeleteDialog(MovieReview review) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除这条影评吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeMovieReview(review.id);
|
||||
Navigator.pop(context);
|
||||
_loadReviews();
|
||||
ToastUtil.show(context, '已删除');
|
||||
},
|
||||
child: const Text('删除', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除这条影评吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeMovieReview(review.id);
|
||||
Navigator.pop(context);
|
||||
_loadReviews();
|
||||
ToastUtil.show(context, '已删除');
|
||||
},
|
||||
child: Text('删除', style: TextStyle(color: colors.error)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,21 +24,22 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
backgroundColor: colors.surfaceContainerHighest,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)),
|
||||
icon: Icon(Icons.close, color: colors.onSurface),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
title: Text(
|
||||
'分享海报',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
@@ -51,12 +52,12 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text(
|
||||
: Text(
|
||||
'分享',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -77,13 +78,14 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
||||
|
||||
/// 构建海报 Widget
|
||||
Widget _buildPosterWidget() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final movie = widget.movie;
|
||||
final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty;
|
||||
|
||||
return Container(
|
||||
width: 320,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -118,10 +120,10 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
||||
// 标题
|
||||
Text(
|
||||
movie.title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -130,9 +132,9 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
movie.alternateTitles.join(' / '),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -158,11 +160,11 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
Text(
|
||||
'/ 10',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -172,44 +174,45 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
||||
|
||||
// 导演
|
||||
if (movie.directors.isNotEmpty)
|
||||
_buildInfoRow('导演', movie.directors.join(' / ')),
|
||||
_buildInfoRow('导演', movie.directors.join(' / '), colors),
|
||||
|
||||
// 编剧
|
||||
if (movie.writers.isNotEmpty)
|
||||
_buildInfoRow('编剧', movie.writers.join(' / ')),
|
||||
_buildInfoRow('编剧', movie.writers.join(' / '), colors),
|
||||
|
||||
// 主演
|
||||
if (movie.actors.isNotEmpty)
|
||||
_buildInfoRow('主演', movie.actors.take(3).join(' / ')),
|
||||
_buildInfoRow('主演', movie.actors.take(3).join(' / '), colors),
|
||||
|
||||
// 类型
|
||||
if (movie.genres.isNotEmpty)
|
||||
_buildInfoRow('类型', movie.genres.join(' / ')),
|
||||
_buildInfoRow('类型', movie.genres.join(' / '), colors),
|
||||
|
||||
// 上映日期
|
||||
if (movie.releaseDate != null)
|
||||
_buildInfoRow(
|
||||
'上映',
|
||||
'${movie.releaseDate!.year}.${movie.releaseDate!.month.toString().padLeft(2, '0')}.${movie.releaseDate!.day.toString().padLeft(2, '0')}',
|
||||
colors,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 简介
|
||||
if (movie.summary != null && movie.summary!.isNotEmpty) ...[
|
||||
const Text(
|
||||
Text(
|
||||
'简介',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
movie.summary!,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
height: 1.6,
|
||||
),
|
||||
maxLines: 5,
|
||||
@@ -220,7 +223,7 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 底部标识
|
||||
const Divider(height: 1, color: Color(0xFFE8E8E8)),
|
||||
Divider(height: 1, color: colors.outline),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -228,14 +231,14 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
||||
Icon(
|
||||
Icons.movie_outlined,
|
||||
size: 14,
|
||||
color: const Color(0xFF1A1A1A).withOpacity(0.5),
|
||||
color: colors.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'来自 MookNote',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: const Color(0xFF1A1A1A).withOpacity(0.5),
|
||||
color: colors.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -249,7 +252,7 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
||||
}
|
||||
|
||||
/// 构建信息行
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
Widget _buildInfoRow(String label, String value, ColorScheme colors) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
@@ -257,17 +260,17 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
||||
children: [
|
||||
Text(
|
||||
'$label:',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF333333),
|
||||
color: colors.onSurface.withValues(alpha: 0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -17,7 +17,7 @@ class MovieTabPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MovieTabPageState extends State<MovieTabPage> {
|
||||
int _layoutStyle = 0; // 0: 海报网格, 1: 列表
|
||||
int _layoutStyle = 0;
|
||||
bool _firstLoad = true;
|
||||
|
||||
@override
|
||||
@@ -31,10 +31,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
children: [
|
||||
const MovieStatusBar(),
|
||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
||||
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||
Expanded(
|
||||
child: _buildMovieList(context),
|
||||
),
|
||||
@@ -43,12 +44,12 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
}
|
||||
|
||||
Widget _buildMovieList(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final statusMap = {0: 'watched', 1: 'watching', 2: 'want_to_watch'};
|
||||
final currentStatus = statusMap[provider.movieStatusIndex]!;
|
||||
final allMovies = provider.movies.where((m) => !m.isDeleted).toList();
|
||||
// 首次加载且数据为空时才显示骨架屏
|
||||
if (_firstLoad && allMovies.isEmpty) {
|
||||
return _buildSkeleton();
|
||||
}
|
||||
@@ -59,8 +60,8 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
if (movies.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await provider.loadMovies(),
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: Colors.white,
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.surface,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [_buildEmptyState(context, provider.movieStatusIndex)],
|
||||
@@ -77,10 +78,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
}
|
||||
|
||||
Widget _buildGridView(List movies, AppProvider provider) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await provider.loadMovies(),
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: Colors.white,
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.surface,
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 100),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
@@ -96,10 +98,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
}
|
||||
|
||||
Widget _buildListView(List movies, AppProvider provider) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await provider.loadMovies(),
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: Colors.white,
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.surface,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
||||
itemCount: movies.length,
|
||||
@@ -109,6 +112,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
}
|
||||
|
||||
Widget _buildListCard(movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: movie),
|
||||
onLongPress: () => _showDeleteDialog(context, movie),
|
||||
@@ -116,23 +120,22 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 海报缩略图
|
||||
Container(
|
||||
width: 48, height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F0F0),
|
||||
color: colors.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: movie.posterPath != null && movie.posterPath!.isNotEmpty
|
||||
? Image.file(File(movie.posterPath!), fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const Icon(Icons.movie_outlined, size: 22, color: Color(0xFFCCCCCC)))
|
||||
: const Icon(Icons.movie_outlined, size: 22, color: Color(0xFFCCCCCC)),
|
||||
errorBuilder: (_, __, ___) => Icon(Icons.movie_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)))
|
||||
: Icon(Icons.movie_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -140,11 +143,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(movie.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
if (movie.alternateTitles.isNotEmpty) ...[
|
||||
const SizedBox(height: 3),
|
||||
Text(movie.alternateTitles.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
|
||||
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
],
|
||||
const SizedBox(height: 6),
|
||||
if (movie.rating != null)
|
||||
@@ -155,7 +158,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
|
||||
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -163,19 +166,20 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
}
|
||||
|
||||
void _showDeleteDialog(BuildContext context, movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
content: Text('确定要删除《${movie.title}》吗?删除后可在回收站恢复。',
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)),
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
@@ -183,7 +187,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red, foregroundColor: Colors.white, elevation: 0,
|
||||
backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
@@ -200,6 +204,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
}
|
||||
|
||||
Widget _buildListSkeleton() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
||||
itemCount: 6,
|
||||
@@ -207,7 +212,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F8F8),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Row(
|
||||
@@ -235,6 +240,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context, int statusIndex) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final statusText = ['已看', '在看', '想看'][statusIndex];
|
||||
return Center(
|
||||
child: Column(
|
||||
@@ -242,11 +248,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
children: [
|
||||
Container(
|
||||
width: 80, height: 80,
|
||||
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(20)),
|
||||
child: const Icon(Icons.movie_outlined, size: 40, color: Color(0xFFCCCCCC)),
|
||||
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)),
|
||||
child: Icon(Icons.movie_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text('暂无$statusText的影片', style: const TextStyle(fontSize: 16, color: Color(0xFF999999))),
|
||||
Text('暂无$statusText的影片', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const SizedBox(height: 24),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
@@ -255,8 +261,8 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(8)),
|
||||
child: const Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(8)),
|
||||
child: Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -22,13 +22,14 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final note = context.watch<AppProvider>().notes.firstWhere(
|
||||
(n) => n.id == widget.note.id,
|
||||
orElse: () => widget.note,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
note.title.isNotEmpty
|
||||
@@ -42,22 +43,21 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
// 日期 + 字数
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
decoration: const BoxDecoration(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
|
||||
bottom: BorderSide(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${note.createdAt.day}',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w200,
|
||||
color: Color(0xFF333333),
|
||||
color: colors.onSurface.withValues(alpha: 0.75),
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
@@ -68,48 +68,44 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
children: [
|
||||
Text(
|
||||
'${note.createdAt.year}/${note.createdAt.month.toString().padLeft(2, '0')} 周${_weekdays[note.createdAt.weekday - 1]}',
|
||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: Color(0xFF777777)),
|
||||
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.55)),
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text(
|
||||
'${note.createdAt.hour.toString().padLeft(2, '0')}:${note.createdAt.minute.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
|
||||
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${note.content.length} 字',
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)),
|
||||
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 标签行
|
||||
if (note.tags.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: _buildTagRow(note.tags),
|
||||
),
|
||||
|
||||
// 内容
|
||||
Expanded(
|
||||
child: Markdown(
|
||||
data: note.content,
|
||||
styleSheet: _buildMarkdownStyleSheet(),
|
||||
styleSheet: _buildMarkdownStyleSheet(colors),
|
||||
padding: const EdgeInsets.all(16),
|
||||
// ignore: deprecated_member_use
|
||||
imageBuilder: (uri, title, alt) => _buildMarkdownImage(uri, note),
|
||||
),
|
||||
),
|
||||
|
||||
// 图片行
|
||||
if (note.images.isNotEmpty) _buildImageRow(note.images),
|
||||
],
|
||||
),
|
||||
|
||||
// 右下角悬浮按钮组
|
||||
Positioned(
|
||||
right: 16,
|
||||
bottom: 24,
|
||||
@@ -121,6 +117,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
}
|
||||
|
||||
Widget _buildTagRow(List<String> tags) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
height: 28,
|
||||
margin: const EdgeInsets.only(bottom: 2),
|
||||
@@ -133,12 +130,12 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
tags[index],
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF666666)),
|
||||
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6)),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -147,12 +144,13 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
}
|
||||
|
||||
Widget _buildImageRow(List<String> images) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: const Border(
|
||||
top: BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
|
||||
color: colors.surface,
|
||||
border: Border(
|
||||
top: BorderSide(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -175,15 +173,15 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Image.file(
|
||||
File(images[index]),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
child: const Icon(Icons.broken_image_outlined, size: 20, color: Color(0xFFCCCCCC)),
|
||||
color: colors.surfaceContainerHighest,
|
||||
child: Icon(Icons.broken_image_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -193,37 +191,37 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
MarkdownStyleSheet _buildMarkdownStyleSheet() {
|
||||
MarkdownStyleSheet _buildMarkdownStyleSheet(ColorScheme colors) {
|
||||
return MarkdownStyleSheet(
|
||||
h1: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4),
|
||||
h2: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4),
|
||||
h3: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4),
|
||||
h4: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4),
|
||||
p: const TextStyle(fontSize: 15, color: Color(0xFF333333), height: 1.8),
|
||||
code: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A), backgroundColor: Color(0xFFF5F5F5), fontFamily: 'monospace'),
|
||||
h1: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4),
|
||||
h2: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4),
|
||||
h3: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4),
|
||||
h4: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4),
|
||||
p: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.75), height: 1.8),
|
||||
code: TextStyle(fontSize: 14, color: colors.onSurface, backgroundColor: colors.surfaceContainerHighest, fontFamily: 'monospace'),
|
||||
codeblockDecoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F8F8),
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
color: colors.surfaceContainerHigh,
|
||||
border: Border.all(color: colors.outline),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
codeblockPadding: const EdgeInsets.all(12),
|
||||
blockquote: const TextStyle(fontSize: 15, color: Color(0xFF666666), fontStyle: FontStyle.italic, height: 1.8),
|
||||
blockquoteDecoration: const BoxDecoration(
|
||||
border: Border(left: BorderSide(color: Color(0xFF999999), width: 4)),
|
||||
blockquote: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.6), fontStyle: FontStyle.italic, height: 1.8),
|
||||
blockquoteDecoration: BoxDecoration(
|
||||
border: Border(left: BorderSide(color: colors.onSurface.withValues(alpha: 0.4), width: 4)),
|
||||
),
|
||||
blockquotePadding: const EdgeInsets.only(left: 12, top: 4, bottom: 4),
|
||||
listBullet: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
|
||||
listBullet: TextStyle(fontSize: 15, color: colors.onSurface),
|
||||
listIndent: 24,
|
||||
a: const TextStyle(fontSize: 15, color: Color(0xFF4A90D9), decoration: TextDecoration.underline),
|
||||
tableHead: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
||||
tableBody: const TextStyle(fontSize: 14, color: Color(0xFF333333)),
|
||||
tableBorder: TableBorder.all(color: const Color(0xFFE5E5E5), width: 0.5),
|
||||
tableHead: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||
tableBody: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75)),
|
||||
tableBorder: TableBorder.all(color: colors.outline, width: 0.5),
|
||||
tableColumnWidth: const FlexColumnWidth(),
|
||||
tableCellsDecoration: const BoxDecoration(color: Colors.white),
|
||||
tableCellsDecoration: BoxDecoration(color: colors.surface),
|
||||
tablePadding: const EdgeInsets.all(8),
|
||||
strong: const TextStyle(fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
||||
em: const TextStyle(fontStyle: FontStyle.italic, color: Color(0xFF333333)),
|
||||
del: const TextStyle(decoration: TextDecoration.lineThrough, color: Color(0xFF999999)),
|
||||
strong: TextStyle(fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||
em: TextStyle(fontStyle: FontStyle.italic, color: colors.onSurface.withValues(alpha: 0.75)),
|
||||
del: TextStyle(decoration: TextDecoration.lineThrough, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -272,7 +270,6 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 截取内容前N个字作为标题
|
||||
String _truncateContent(String content) {
|
||||
final cleaned = content
|
||||
.replaceAll(RegExp(r'^#+\s+', multiLine: true), '')
|
||||
@@ -298,8 +295,8 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
});
|
||||
}
|
||||
|
||||
/// 右下角悬浮按钮组
|
||||
Widget _buildFloatingActionButtons() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -307,13 +304,16 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
icon: Icons.edit_outlined,
|
||||
onPressed: () => _navigateToEdit(context),
|
||||
tooltip: '编辑',
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildFloatingButton(
|
||||
icon: Icons.delete_outline,
|
||||
onPressed: () => _showDeleteDialog(context),
|
||||
tooltip: '删除',
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: colors.error,
|
||||
foregroundColor: colors.onError,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildFloatingButton(
|
||||
@@ -321,6 +321,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
onPressed: _shareNote,
|
||||
tooltip: '分享',
|
||||
backgroundColor: const Color(0xFF4CAF50),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -330,7 +331,8 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
required IconData icon,
|
||||
required VoidCallback onPressed,
|
||||
required String tooltip,
|
||||
Color backgroundColor = const Color(0xFF1A1A1A),
|
||||
required Color backgroundColor,
|
||||
required Color foregroundColor,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
@@ -349,7 +351,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
],
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(icon, size: 18, color: Colors.white),
|
||||
icon: Icon(icon, size: 18, color: foregroundColor),
|
||||
onPressed: onPressed,
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: tooltip,
|
||||
@@ -375,7 +377,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
context.read<AppProvider>().removeNote(widget.note.id);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('删除', style: TextStyle(color: Colors.red)),
|
||||
child: Text('删除', style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -59,8 +59,9 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: AppBar(
|
||||
title: GestureDetector(
|
||||
@@ -73,8 +74,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
child: TextButton(
|
||||
onPressed: _saveNote,
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF1A1A1A),
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -96,9 +97,9 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
// 顶部信息栏
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
decoration: const BoxDecoration(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
|
||||
bottom: BorderSide(color: colors.outline, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
@@ -110,10 +111,10 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
// 大日号
|
||||
Text(
|
||||
'${_createdAt.day}',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w200,
|
||||
color: Color(0xFF333333),
|
||||
color: colors.onSurface.withValues(alpha: 0.75),
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
@@ -125,18 +126,18 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
children: [
|
||||
Text(
|
||||
'${_createdAt.year}/${_createdAt.month.toString().padLeft(2, '0')} 周${_weekdays[_createdAt.weekday - 1]}',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF777777),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text(
|
||||
'${_createdAt.hour.toString().padLeft(2, '0')}:${_createdAt.minute.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -145,9 +146,9 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
// 字数统计
|
||||
Text(
|
||||
'$_charCount 字',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFFAAAAAA),
|
||||
color: colors.onSurface.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -208,13 +209,14 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
|
||||
/// 现代极简底部工具栏
|
||||
Widget _buildFloatingToolbar() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(12, 8, 12, 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFEAEAEA), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
@@ -251,7 +253,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
// 视图模式切换
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 1, height: 20, color: const Color(0xFFE5E5E5),
|
||||
width: 1, height: 20, color: colors.outline,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_modeSwitch(Icons.edit, 'edit'),
|
||||
@@ -262,6 +264,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
}
|
||||
|
||||
Widget _toolBtn(IconData icon, String tooltip, VoidCallback onTap) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
@@ -271,7 +274,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
message: tooltip,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 6),
|
||||
child: Icon(icon, size: 21, color: const Color(0xFF555555)),
|
||||
child: Icon(icon, size: 21, color: colors.onSurface.withValues(alpha: 0.7)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -279,16 +282,18 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
}
|
||||
|
||||
Widget _toolGap() {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: SizedBox(
|
||||
height: 18,
|
||||
child: VerticalDivider(width: 0, thickness: 0.5, color: Color(0xFFE0E0E0)),
|
||||
child: VerticalDivider(width: 0, thickness: 0.5, color: colors.outline),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _modeSwitch(IconData icon, String mode) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final active = _editorMode == mode;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
@@ -298,13 +303,13 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? const Color(0xFF1A1A1A) : Colors.transparent,
|
||||
color: active ? colors.primary : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: active ? Colors.white : const Color(0xFFAAAAAA),
|
||||
color: active ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -374,6 +379,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
|
||||
/// 编辑器
|
||||
Widget _buildEditor() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return TextField(
|
||||
controller: _contentController,
|
||||
maxLines: null,
|
||||
@@ -384,40 +390,41 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
height: 1.6,
|
||||
fontSize: 16,
|
||||
),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.6,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
hintText: '使用 Markdown 格式书写...',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
height: 1.6,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
contentPadding: EdgeInsets.all(16),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 实时 Markdown 预览(点击回到编辑)
|
||||
Widget _buildPreview() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final text = _contentController.text;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _editorMode = 'edit'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
child: text.isEmpty
|
||||
? const Center(
|
||||
? Center(
|
||||
child: Text(
|
||||
'预览区域',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -426,45 +433,45 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
selectable: true,
|
||||
padding: const EdgeInsets.all(16),
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
h1: const TextStyle(
|
||||
h1: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.4,
|
||||
),
|
||||
h2: const TextStyle(
|
||||
h2: TextStyle(
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.4,
|
||||
),
|
||||
h3: const TextStyle(
|
||||
h3: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.4,
|
||||
),
|
||||
p: const TextStyle(
|
||||
p: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF333333),
|
||||
color: colors.onSurface.withValues(alpha: 0.75),
|
||||
height: 1.7,
|
||||
),
|
||||
code: const TextStyle(
|
||||
code: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF1A1A1A),
|
||||
backgroundColor: Color(0xFFF0F0F0),
|
||||
color: colors.onSurface,
|
||||
backgroundColor: colors.outlineVariant,
|
||||
),
|
||||
codeblockDecoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F0F0),
|
||||
color: colors.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
blockquote: const TextStyle(
|
||||
blockquote: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
blockquoteDecoration: const BoxDecoration(
|
||||
blockquoteDecoration: BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(color: Color(0xFFCCCCCC), width: 3),
|
||||
left: BorderSide(color: colors.onSurface.withValues(alpha: 0.25), width: 3),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -494,10 +501,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
}
|
||||
|
||||
Widget _buildTagChip(int index) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
@@ -505,12 +513,12 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
children: [
|
||||
Text(
|
||||
_tags[index],
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF666666)),
|
||||
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6)),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => _tags.removeAt(index)),
|
||||
child: const Icon(Icons.close, size: 10, color: Color(0xFFBBBBBB)),
|
||||
child: Icon(Icons.close, size: 10, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -518,20 +526,21 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
}
|
||||
|
||||
Widget _buildAddTagButton() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: _showAddTagDialog,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: const Color(0xFFDDDDDD), width: 1),
|
||||
border: Border.all(color: colors.onSurface.withValues(alpha: 0.25), width: 1),
|
||||
),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.add, size: 12, color: Color(0xFFAAAAAA)),
|
||||
SizedBox(width: 2),
|
||||
Text('标签', style: TextStyle(fontSize: 11, color: Color(0xFFAAAAAA))),
|
||||
Icon(Icons.add, size: 12, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||
const SizedBox(width: 2),
|
||||
Text('标签', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -550,18 +559,20 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return StatefulBuilder(
|
||||
builder: (ctx, setDialogState) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
title: const Text(
|
||||
title: Text(
|
||||
'添加标签',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
||||
@@ -575,13 +586,13 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
||||
cursorColor: const Color(0xFF1A1A1A),
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||
cursorColor: colors.primary,
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入新标签名称',
|
||||
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFBBBBBB)),
|
||||
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFF8F8F8),
|
||||
fillColor: colors.surfaceContainerHigh,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
@@ -593,11 +604,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1),
|
||||
borderSide: BorderSide(color: colors.primary, width: 1),
|
||||
),
|
||||
suffixIcon: controller.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear, size: 16, color: Color(0xFFAAAAAA)),
|
||||
icon: Icon(Icons.clear, size: 16, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||
onPressed: () => controller.clear(),
|
||||
)
|
||||
: null,
|
||||
@@ -613,11 +624,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
// 已有标签列表
|
||||
if (availableTags.isNotEmpty) ...[
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
Text(
|
||||
'或选择已有标签',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFFAAAAAA),
|
||||
color: colors.onSurface.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -637,15 +648,15 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outline, width: 0.5),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF555555),
|
||||
color: colors.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -673,7 +684,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF999999),
|
||||
foregroundColor: colors.onSurface.withValues(alpha: 0.4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
child: const Text('取消', style: TextStyle(fontSize: 14)),
|
||||
@@ -684,8 +695,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF1A1A1A),
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
@@ -695,10 +706,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
],
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// 获取所有已有标签(从所有笔记中收集)
|
||||
List<String> _getAllExistingTags(AppProvider provider) {
|
||||
final allTags = <String>{};
|
||||
@@ -717,6 +729,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
}
|
||||
|
||||
Widget _buildAppBarTitle() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final base = _isEditing ? '编辑笔记' : '新建笔记';
|
||||
final t = _titleController.text.trim();
|
||||
if (t.isEmpty) {
|
||||
@@ -725,7 +738,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
children: [
|
||||
Text(base),
|
||||
const SizedBox(width: 6),
|
||||
const Icon(Icons.edit, size: 14, color: Color(0xFF999999)),
|
||||
Icon(Icons.edit, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -738,8 +751,10 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
final controller = TextEditingController(text: _titleController.text);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
builder: (context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text(
|
||||
@@ -749,23 +764,23 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
style: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A)),
|
||||
style: TextStyle(fontSize: 16, color: colors.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入标题...',
|
||||
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
|
||||
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFFAFAFA),
|
||||
fillColor: colors.surfaceContainerHigh,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
|
||||
borderSide: BorderSide(color: colors.outline, width: 0.5),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
|
||||
borderSide: BorderSide(color: colors.outline, width: 0.5),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1),
|
||||
borderSide: BorderSide(color: colors.primary, width: 1),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
@@ -778,7 +793,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF666666),
|
||||
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
@@ -788,8 +803,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF1A1A1A),
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
@@ -797,7 +812,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
),
|
||||
],
|
||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -826,7 +842,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
} else {
|
||||
// 添加新笔记 - 先创建笔记获取ID
|
||||
final noteId = now.millisecondsSinceEpoch.toString();
|
||||
|
||||
|
||||
// 如果有图片,需要移动到正确的ID目录
|
||||
List<String> finalImages = [];
|
||||
if (_images.isNotEmpty) {
|
||||
@@ -835,7 +851,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
final newNoteId = noteId;
|
||||
finalImages = await _moveImagesToNewId(oldNoteId, newNoteId);
|
||||
}
|
||||
|
||||
|
||||
final title = _titleController.text.trim();
|
||||
|
||||
final newNote = Note(
|
||||
@@ -856,17 +872,17 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
|
||||
// 刷新笔记列表
|
||||
await context.read<AppProvider>().loadNotes();
|
||||
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
|
||||
/// 将图片从临时ID目录移动到新ID目录
|
||||
Future<List<String>> _moveImagesToNewId(String oldNoteId, String newNoteId) async {
|
||||
final List<String> newPaths = [];
|
||||
|
||||
|
||||
final newDir = await ImagePathHelper.instance.getNoteImagesDir(newNoteId);
|
||||
|
||||
|
||||
for (final imagePath in _images) {
|
||||
// 使用路径分隔符检查,兼容 Windows 和 Unix
|
||||
final normalizedPath = imagePath.replaceAll('\\', '/');
|
||||
@@ -874,9 +890,9 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
// 需要移动的文件
|
||||
final fileName = p.basename(imagePath);
|
||||
final newPath = p.join(newDir, fileName);
|
||||
|
||||
|
||||
await ImagePathHelper.instance.ensureDirExists(newDir);
|
||||
|
||||
|
||||
// 检查源文件是否存在
|
||||
final sourceFile = File(imagePath);
|
||||
if (await sourceFile.exists()) {
|
||||
@@ -888,14 +904,14 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
newPaths.add(imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 删除旧目录
|
||||
try {
|
||||
await ImagePathHelper.instance.deleteNoteImages(oldNoteId);
|
||||
} catch (e) {
|
||||
// 忽略删除失败
|
||||
}
|
||||
|
||||
|
||||
return newPaths;
|
||||
}
|
||||
|
||||
@@ -908,11 +924,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
maxHeight: 1920,
|
||||
imageQuality: 85,
|
||||
);
|
||||
|
||||
|
||||
if (image != null) {
|
||||
// 生成唯一的文件名
|
||||
final fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
|
||||
|
||||
// 如果是编辑模式,使用现有笔记ID;如果是新建模式,使用临时ID(保存时会替换)
|
||||
String noteId;
|
||||
if (_isEditing) {
|
||||
@@ -922,14 +938,14 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
noteId = _tempNoteId ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
_tempNoteId = noteId;
|
||||
}
|
||||
|
||||
|
||||
// 复制图片到应用目录: images/notes/{noteId}/{fileName}
|
||||
final targetDir = await ImagePathHelper.instance.getNoteImagesDir(noteId);
|
||||
await ImagePathHelper.instance.ensureDirExists(targetDir);
|
||||
final targetPath = p.join(targetDir, fileName);
|
||||
|
||||
|
||||
await File(image.path).copy(targetPath);
|
||||
|
||||
|
||||
setState(() => _images.add(targetPath));
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -939,11 +955,12 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
|
||||
/// 构建图片横向滚动行
|
||||
Widget _buildImageRow() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
if (_images.isEmpty) {
|
||||
return Container(
|
||||
height: 80,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
),
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
@@ -958,7 +975,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
return Container(
|
||||
height: 88,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.02),
|
||||
@@ -984,6 +1001,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
|
||||
/// 添加图片按钮
|
||||
Widget _buildAddImageButton() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: _pickImage,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -992,13 +1010,13 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: const Color(0xFFF8F8F8),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
color: colors.surfaceContainerHigh,
|
||||
border: Border.all(color: colors.outline, width: 0.5),
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.add_photo_alternate_outlined,
|
||||
size: 24,
|
||||
color: Color(0xFFBBBBBB),
|
||||
color: colors.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1006,6 +1024,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
|
||||
/// 构建图片项
|
||||
Widget _buildImageItem(int index) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () => _showImagePreview(index),
|
||||
onLongPress: () => _showDeleteImageDialog(index),
|
||||
@@ -1015,7 +1034,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outline, width: 0.5),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Image.file(
|
||||
@@ -1056,8 +1075,10 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
void _showDeleteImageDialog(int index) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
builder: (context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text(
|
||||
@@ -1067,11 +1088,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
content: const Text(
|
||||
content: Text(
|
||||
'确定要删除这张图片吗?此操作不可恢复。',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -1079,7 +1100,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF666666),
|
||||
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
child: const Text('取消'),
|
||||
@@ -1090,8 +1111,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colors.error,
|
||||
foregroundColor: colors.onError,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -1102,7 +1123,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
),
|
||||
],
|
||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,21 +26,22 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
backgroundColor: colors.surfaceContainerHighest,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)),
|
||||
icon: Icon(Icons.close, color: colors.onSurface),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
title: Text(
|
||||
'分享笔记',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
@@ -53,12 +54,12 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text(
|
||||
: Text(
|
||||
'分享',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -78,6 +79,7 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
||||
}
|
||||
|
||||
Widget _buildPosterWidget() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final note = widget.note;
|
||||
final hasImages = note.images.isNotEmpty;
|
||||
final dateStr =
|
||||
@@ -88,7 +90,7 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
||||
return Container(
|
||||
width: 320,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -124,10 +126,10 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
||||
if (note.title.isNotEmpty) ...[
|
||||
Text(
|
||||
note.title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -136,9 +138,9 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
||||
// 日期
|
||||
Text(
|
||||
dateStr,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -152,12 +154,12 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F0F0),
|
||||
color: colors.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF888888)),
|
||||
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.5)),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -165,7 +167,7 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
||||
],
|
||||
|
||||
const SizedBox(height: 16),
|
||||
Container(height: 0.5, color: const Color(0xFFE8E8E8)),
|
||||
Container(height: 0.5, color: colors.outline),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 正文内容
|
||||
@@ -173,9 +175,9 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
||||
note.content,
|
||||
maxLines: 12,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF333333),
|
||||
color: colors.onSurface.withValues(alpha: 0.75),
|
||||
height: 1.8,
|
||||
),
|
||||
),
|
||||
@@ -191,11 +193,11 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
||||
],
|
||||
const Spacer(),
|
||||
// Mooknote 品牌
|
||||
const Text(
|
||||
Text(
|
||||
'Mooknote',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -210,14 +212,15 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
||||
}
|
||||
|
||||
Widget _buildMetaChip(IconData icon, String text) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 13, color: const Color(0xFFAAAAAA)),
|
||||
Icon(icon, size: 13, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)),
|
||||
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
bool _hasMore = true;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
int _layoutStyle = 0; // 0: 列表, 1: 瀑布流, 2: 时间线
|
||||
int _layoutStyle = 0;
|
||||
bool _firstLoad = true;
|
||||
|
||||
@override
|
||||
@@ -110,7 +110,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// 笔记内容
|
||||
Expanded(
|
||||
child: Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
@@ -122,10 +121,11 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
}
|
||||
|
||||
if (allNotes.isEmpty && _displayedNotes.isEmpty) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refresh,
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: Colors.white,
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.surface,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [_buildEmptyState(context)],
|
||||
@@ -159,6 +159,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
}
|
||||
|
||||
Widget _buildWaterfallSkeleton() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
||||
child: Row(
|
||||
@@ -168,7 +169,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
children: List.generate(4, (_) => Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 6, offset: const Offset(0, 2)),
|
||||
@@ -200,25 +201,24 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 列表视图 ────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildListView() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refresh,
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: Colors.white,
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.surface,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 100),
|
||||
itemCount: _displayedNotes.length + (_hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= _displayedNotes.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 20, height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A)),
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -229,24 +229,23 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 时间线视图 ──────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildTimelineView() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refresh,
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: Colors.white,
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.surface,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
||||
itemCount: _displayedNotes.length + (_hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= _displayedNotes.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: SizedBox(width: 20, height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A))),
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -257,6 +256,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
}
|
||||
|
||||
Widget _buildTimelineItem(Note note) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) async {
|
||||
@@ -268,72 +268,60 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 左侧时间线
|
||||
SizedBox(
|
||||
width: 40,
|
||||
child: Column(
|
||||
children: [
|
||||
// 圆点
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
border: Border.all(color: colors.surface, width: 2),
|
||||
),
|
||||
),
|
||||
// 连线
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: 1,
|
||||
color: const Color(0xFFE5E5E5),
|
||||
color: colors.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 右侧内容
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 时间
|
||||
Text(
|
||||
_formatFullDate(note.updatedAt),
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
|
||||
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
),
|
||||
|
||||
// 标题
|
||||
if (note.title.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
note.title,
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
|
||||
// 内容预览
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
_getPreviewText(note),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 13, color: Color(0xFF888888), height: 1.5),
|
||||
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5), height: 1.5),
|
||||
),
|
||||
|
||||
// 标签
|
||||
if (note.tags.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
@@ -342,10 +330,10 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
children: note.tags.map((tag) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(tag, style: const TextStyle(fontSize: 10, color: Color(0xFF999999))),
|
||||
child: Text(tag, style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
)).toList(),
|
||||
),
|
||||
],
|
||||
@@ -364,10 +352,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
'${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
// ─── 瀑布流视图 ──────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildWaterfallView() {
|
||||
// 分为左右两列
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final leftItems = <Note>[];
|
||||
final rightItems = <Note>[];
|
||||
for (int i = 0; i < _displayedNotes.length; i++) {
|
||||
@@ -380,8 +366,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refresh,
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: Colors.white,
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.surface,
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
||||
@@ -398,6 +384,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
}
|
||||
|
||||
Widget _buildWaterfallCard(Note note) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final contentText = _getPreviewText(note);
|
||||
final images = note.images;
|
||||
final hasImage = images.isNotEmpty;
|
||||
@@ -413,7 +400,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -428,7 +415,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 顶部图片
|
||||
if (hasImage)
|
||||
Stack(
|
||||
children: [
|
||||
@@ -458,8 +444,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 底部文字区域
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 8, 10, 10),
|
||||
child: Column(
|
||||
@@ -471,47 +455,45 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
note.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
|
||||
if (contentText.isNotEmpty && contentText != '(无内容)') ...[
|
||||
if (note.title.isNotEmpty) const SizedBox(height: 4),
|
||||
Text(
|
||||
contentText,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_formatTime(note.updatedAt),
|
||||
style: const TextStyle(fontSize: 10, color: Color(0xFFCCCCCC)),
|
||||
style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
),
|
||||
),
|
||||
if (note.tags.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
note.tags.first,
|
||||
style: const TextStyle(fontSize: 10, color: Color(0xFF999999)),
|
||||
style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -546,20 +528,21 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
}
|
||||
|
||||
void _showDeleteDialog(BuildContext context, Note note) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
content: const Text('确定要删除这条笔记吗?删除后可在回收站恢复。',
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)),
|
||||
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
content: Text('确定要删除这条笔记吗?删除后可在回收站恢复。',
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF666666),
|
||||
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
child: const Text('取消'),
|
||||
@@ -570,8 +553,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colors.error,
|
||||
foregroundColor: colors.onError,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
@@ -584,8 +567,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 数据同步 ────────────────────────────────────────────────────────
|
||||
|
||||
void _syncDisplayedNotes(List<Note> allNotes) {
|
||||
final validNoteIds = allNotes.map((n) => n.id).toSet();
|
||||
final initialLength = _displayedNotes.length;
|
||||
@@ -611,9 +592,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 空状态 ──────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -621,24 +601,24 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
Container(
|
||||
width: 80, height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(Icons.note_outlined, size: 40, color: Color(0xFFCCCCCC)),
|
||||
child: Icon(Icons.note_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text('暂无笔记', style: TextStyle(fontSize: 16, color: Color(0xFF999999))),
|
||||
Text('暂无笔记', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const SizedBox(height: 24),
|
||||
InkWell(
|
||||
onTap: () => Navigator.pushNamed(context, '/note-form'),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text('添加记录',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
child: Text('添加记录',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -51,12 +51,13 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
children: [
|
||||
AppBar(
|
||||
leading: Builder(
|
||||
builder: (context) => IconButton(
|
||||
icon: const Icon(Icons.menu, color: Color(0xFF1A1A1A)),
|
||||
icon: Icon(Icons.menu, color: colors.onSurface),
|
||||
onPressed: () => Scaffold.of(context).openDrawer(),
|
||||
),
|
||||
),
|
||||
@@ -64,7 +65,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: const Color(0xFFF8F8F8),
|
||||
color: colors.surfaceContainerHigh,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -98,6 +99,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
Widget _buildUserCard() {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final movieCount = provider.movies.where((m) => !m.isDeleted).length;
|
||||
final bookCount = provider.books.length;
|
||||
final noteCount = provider.notes.length;
|
||||
@@ -106,7 +108,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
margin: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)),
|
||||
@@ -122,14 +124,14 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
width: 64, height: 64,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0xFFF5F5F5),
|
||||
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: _avatarPath != null && _avatarPath!.isNotEmpty
|
||||
? Image.file(File(_avatarPath!), fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const Icon(Icons.person_outline, size: 32, color: Color(0xFFCCCCCC)))
|
||||
: const Icon(Icons.person_outline, size: 32, color: Color(0xFFCCCCCC)),
|
||||
errorBuilder: (_, __, ___) => Icon(Icons.person_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.25)))
|
||||
: Icon(Icons.person_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@@ -139,25 +141,25 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => _editNickname(context),
|
||||
child: Text(_nickname, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
child: Text(_nickname, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
GestureDetector(
|
||||
onTap: () => _editMotto(context),
|
||||
child: Text(_motto, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 13, color: Color(0xFFAAAAAA))),
|
||||
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined, color: Color(0xFF999999)),
|
||||
icon: Icon(Icons.settings_outlined, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
onPressed: () => _showSettings(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(height: 1, color: Color(0xFFF0F0F0)),
|
||||
Divider(height: 1, color: colors.outlineVariant),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatRow(Icons.movie_outlined, _formatCount(movieCount), '观影'),
|
||||
const SizedBox(height: 8),
|
||||
@@ -172,13 +174,14 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
}
|
||||
|
||||
Widget _buildStatRow(IconData icon, String count, String label) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 14, color: const Color(0xFFBBBBBB)),
|
||||
Icon(icon, size: 14, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
const SizedBox(width: 8),
|
||||
Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
Text(label, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const Spacer(),
|
||||
Text(count, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text(count, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -192,11 +195,12 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
// ─── 探索行 ──────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildExploreRow() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)),
|
||||
@@ -204,9 +208,9 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.auto_awesome, size: 16, color: Color(0xFFBBBBBB)),
|
||||
Icon(Icons.auto_awesome, size: 16, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
const SizedBox(width: 10),
|
||||
const Text('快捷入口', style: TextStyle(fontSize: 13, color: Color(0xFF888888))),
|
||||
Text('快捷入口', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||
const Spacer(),
|
||||
_buildQuickAction(Icons.explore_outlined, '漫步', () => Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage()))),
|
||||
const SizedBox(width: 24),
|
||||
@@ -217,14 +221,15 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
}
|
||||
|
||||
Widget _buildQuickAction(IconData icon, String label, VoidCallback onTap) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 20, color: const Color(0xFF555555)),
|
||||
Icon(icon, size: 20, color: colors.onSurface.withValues(alpha: 0.7)),
|
||||
const SizedBox(height: 2),
|
||||
Text(label, style: const TextStyle(fontSize: 10, color: Color(0xFF999999))),
|
||||
Text(label, style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -233,17 +238,19 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
// ─── 菜单 ────────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildSectionHeader(String title) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 10),
|
||||
child: Text(title, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFFBBBBBB))),
|
||||
child: Text(title, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuGroup(List<_MenuEntry> entries) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)),
|
||||
@@ -269,19 +276,19 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
Container(
|
||||
width: 36, height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(e.value.icon, size: 18, color: const Color(0xFF666666)),
|
||||
child: Icon(e.value.icon, size: 18, color: colors.onSurface.withValues(alpha: 0.6)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(e.value.title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))),
|
||||
const Icon(Icons.chevron_right, size: 16, color: Color(0xFFD0D0D0)),
|
||||
Expanded(child: Text(e.value.title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface))),
|
||||
Icon(Icons.chevron_right, size: 16, color: colors.onSurface.withValues(alpha: 0.2)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isLast) const Divider(height: 1, indent: 68, endIndent: 20, color: Color(0xFFF0F0F0)),
|
||||
if (!isLast) Divider(height: 1, indent: 68, endIndent: 20, color: colors.outlineVariant),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
@@ -310,16 +317,17 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
}
|
||||
|
||||
void _editNickname(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final controller = TextEditingController(text: _nickname);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white, elevation: 0,
|
||||
backgroundColor: colors.surface, elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('修改昵称', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
content: TextField(controller: controller, decoration: const InputDecoration(hintText: '输入昵称', border: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xFFE5E5E5))))),
|
||||
title: Text('修改昵称', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
content: TextField(controller: controller, decoration: InputDecoration(hintText: '输入昵称', border: UnderlineInputBorder(borderSide: BorderSide(color: colors.outline)))),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消', style: TextStyle(color: Color(0xFF666666)))),
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
|
||||
TextButton(onPressed: () async {
|
||||
final newNickname = controller.text.trim();
|
||||
if (newNickname.isNotEmpty) {
|
||||
@@ -334,16 +342,17 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
}
|
||||
|
||||
void _editMotto(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final controller = TextEditingController(text: _motto);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white, elevation: 0,
|
||||
backgroundColor: colors.surface, elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('修改座右铭', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
content: TextField(controller: controller, maxLines: 2, decoration: const InputDecoration(hintText: '输入座右铭', border: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xFFE5E5E5))))),
|
||||
title: Text('修改座右铭', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
content: TextField(controller: controller, maxLines: 2, decoration: InputDecoration(hintText: '输入座右铭', border: UnderlineInputBorder(borderSide: BorderSide(color: colors.outline)))),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消', style: TextStyle(color: Color(0xFF666666)))),
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
|
||||
TextButton(onPressed: () async {
|
||||
final newMotto = controller.text.trim();
|
||||
await _userPrefs.setMotto(newMotto);
|
||||
@@ -358,34 +367,35 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
// ─── 备份弹窗 ────────────────────────────────────────────────────────
|
||||
|
||||
void _showBackupOptions(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
builder: (ctx) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(width: 36, height: 4, decoration: BoxDecoration(color: const Color(0xFFDDDDDD), borderRadius: BorderRadius.circular(2))),
|
||||
Container(width: 36, height: 4, decoration: BoxDecoration(color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
|
||||
const SizedBox(height: 20),
|
||||
const Align(alignment: Alignment.centerLeft, child: Text('选择备份方式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)))),
|
||||
Align(alignment: Alignment.centerLeft, child: Text('选择备份方式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface))),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.folder_outlined, color: Color(0xFF666666))),
|
||||
title: const Text('本地备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
||||
subtitle: const Text('备份到本地文件夹,支持恢复', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
trailing: const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC)),
|
||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.folder_outlined, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
title: Text('本地备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
subtitle: Text('备份到本地文件夹,支持恢复', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
onTap: () { Navigator.pop(ctx); _push(context, const BackupPage()); },
|
||||
),
|
||||
const Divider(height: 0.5, color: Color(0xFFF0F0F0)),
|
||||
Divider(height: 0.5, color: colors.outlineVariant),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.cloud_outlined, color: Color(0xFF666666))),
|
||||
title: const Text('云备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
||||
subtitle: const Text('通过 WebDAV 同步到云端', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
trailing: const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC)),
|
||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.cloud_outlined, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
title: Text('云备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
subtitle: Text('通过 WebDAV 同步到云端', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
onTap: () { Navigator.pop(ctx); _push(context, const CloudSyncPage()); },
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@@ -421,17 +431,20 @@ class SettingsPage extends StatefulWidget {
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
final UserPrefs _userPrefs = UserPrefs();
|
||||
bool _hideBottomNavOnScroll = true;
|
||||
int _themeMode = 0; // 0=系统, 1=浅色, 2=深色
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_hideBottomNavOnScroll = _userPrefs.hideBottomNavOnScroll;
|
||||
_themeMode = _userPrefs.themeMode;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(title: const Text('设置')),
|
||||
body: ListView(
|
||||
children: [
|
||||
@@ -442,7 +455,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
subtitle: '清理未在数据库中引用的图片文件',
|
||||
onTap: () => _showClearCacheDialog(context),
|
||||
),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildSwitchItem(
|
||||
icon: Icons.swipe_vertical_outlined,
|
||||
title: '底部导航栏滚动隐藏',
|
||||
@@ -450,28 +463,31 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
value: _hideBottomNavOnScroll,
|
||||
onChanged: _toggleHideBottomNavOnScroll,
|
||||
),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildNavigationItem(
|
||||
icon: Icons.apps_outlined,
|
||||
title: '应用图标',
|
||||
subtitle: '更换桌面应用图标',
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AppIconPickerPage())),
|
||||
),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildNavigationItem(
|
||||
icon: Icons.view_list_outlined,
|
||||
title: '主界面设置',
|
||||
subtitle: '启动标签、模块显示开关',
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MainContentSettingsPage())),
|
||||
),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildNavigationItem(
|
||||
icon: Icons.dashboard_outlined,
|
||||
title: '布局设置',
|
||||
subtitle: '笔记、影视、阅读的展示样式',
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const LayoutSettingsPage())),
|
||||
),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildSectionHeader('外观'),
|
||||
_buildThemeModeSelector(),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildSectionHeader('帮助'),
|
||||
_buildLinkItem(
|
||||
context: context,
|
||||
@@ -480,7 +496,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
subtitle: '查看应用使用指南',
|
||||
url: 'https://mooknote.iletter.top/#/guide',
|
||||
),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -491,23 +507,93 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
setState(() => _hideBottomNavOnScroll = value);
|
||||
}
|
||||
|
||||
static const _themeModeLabels = ['跟随系统', '浅色模式', '深色模式'];
|
||||
static const _themeModeIcons = [Icons.brightness_auto, Icons.light_mode, Icons.dark_mode];
|
||||
|
||||
Widget _buildThemeModeSelector() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () => _showThemeModePicker(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(_themeModeIcons[_themeMode], color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('主题模式', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
const SizedBox(height: 2),
|
||||
Text(_themeModeLabels[_themeMode], style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
]),
|
||||
),
|
||||
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showThemeModePicker() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) => Container(
|
||||
decoration: BoxDecoration(color: colors.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(16))),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(width: 36, height: 4, decoration: BoxDecoration(color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
|
||||
const SizedBox(height: 20),
|
||||
Align(alignment: Alignment.centerLeft, child: Padding(padding: const EdgeInsets.symmetric(horizontal: 24), child: Text('主题模式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)))),
|
||||
const SizedBox(height: 16),
|
||||
for (int i = 0; i < _themeModeLabels.length; i++)
|
||||
ListTile(
|
||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(_themeModeIcons[i], color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
title: Text(_themeModeLabels[i], style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
trailing: _themeMode == i ? Icon(Icons.check, color: colors.onSurface, size: 20) : null,
|
||||
onTap: () async { await _setThemeMode(i); Navigator.pop(ctx); },
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _setThemeMode(int mode) async {
|
||||
await _userPrefs.setThemeMode(mode);
|
||||
final themeMode = switch (mode) {
|
||||
1 => ThemeMode.light,
|
||||
2 => ThemeMode.dark,
|
||||
_ => ThemeMode.system,
|
||||
};
|
||||
if (mounted) {
|
||||
context.read<AppProvider>().setThemeMode(themeMode);
|
||||
setState(() => _themeMode = mode);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSwitchItem({required IconData icon, required String title, required String subtitle, required bool value, required ValueChanged<bool> onChanged}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () => onChanged(!value),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)),
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
||||
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
const SizedBox(height: 2),
|
||||
Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
]),
|
||||
),
|
||||
Switch(value: value, onChanged: onChanged, activeColor: const Color(0xFF1A1A1A), activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), inactiveThumbColor: Colors.white, inactiveTrackColor: const Color(0xFFE5E5E5)),
|
||||
Switch(value: value, onChanged: onChanged, activeColor: colors.primary, activeTrackColor: colors.primary.withOpacity(0.3), inactiveThumbColor: colors.surface, inactiveTrackColor: colors.outline),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -515,29 +601,31 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 32, 24, 12),
|
||||
child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavigationItem({required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)),
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
||||
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
const SizedBox(height: 2),
|
||||
Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
]),
|
||||
),
|
||||
const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC), size: 20),
|
||||
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -545,22 +633,23 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
|
||||
Widget _buildLinkItem({required BuildContext context, required IconData icon, required String title, required String subtitle, required String url}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () => _launchUrl(context, url),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)),
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
||||
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
const SizedBox(height: 2),
|
||||
Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
]),
|
||||
),
|
||||
const Icon(Icons.open_in_new, color: Color(0xFFCCCCCC), size: 18),
|
||||
Icon(Icons.open_in_new, color: colors.onSurface.withValues(alpha: 0.25), size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -568,19 +657,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
|
||||
Widget _buildActionItem({required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)),
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
||||
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
const SizedBox(height: 2),
|
||||
Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
]),
|
||||
),
|
||||
],
|
||||
@@ -590,19 +680,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
|
||||
void _showClearCacheDialog(BuildContext pageContext) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
context: pageContext,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
backgroundColor: Colors.white, elevation: 0,
|
||||
backgroundColor: colors.surface, elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('清除缓存数据'),
|
||||
content: const Text('这将删除所有未在数据库中引用的图片文件。确定要继续吗?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('取消', style: TextStyle(color: Color(0xFF666666)))),
|
||||
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
|
||||
TextButton(onPressed: () async {
|
||||
Navigator.pop(dialogContext);
|
||||
await _clearCacheData(pageContext);
|
||||
}, child: const Text('确定', style: TextStyle(color: Colors.red))),
|
||||
}, child: Text('确定', style: TextStyle(color: colors.error))),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -712,24 +803,25 @@ class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(title: const Text('主界面设置')),
|
||||
body: ListView(
|
||||
children: [
|
||||
_buildSectionHeader('启动设置'),
|
||||
_buildDefaultTabSelector(),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildSectionHeader('模块开关'),
|
||||
_buildSwitchItem(Icons.movie_outlined, '观影', '记录和管理观影记录', _showMovieTab, _toggleMovieTab),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildSwitchItem(Icons.menu_book_outlined, '阅读', '记录和管理阅读记录', _showBookTab, _toggleBookTab),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildSwitchItem(Icons.note_outlined, '笔记', '记录和管理笔记', _showNoteTab, _toggleNoteTab),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||
child: const Text('至少保留一个模块,关闭后对应标签页将不再显示。', style: TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))),
|
||||
child: Text('至少保留一个模块,关闭后对应标签页将不再显示。', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -737,37 +829,40 @@ class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12),
|
||||
child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSwitchItem(IconData icon, String title, String subtitle, bool value, ValueChanged<bool> onChanged) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)),
|
||||
title: Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
||||
subtitle: Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
trailing: Switch(value: value, onChanged: onChanged, activeColor: const Color(0xFF1A1A1A), activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), inactiveThumbColor: Colors.white, inactiveTrackColor: const Color(0xFFE5E5E5)),
|
||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||
title: Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
trailing: Switch(value: value, onChanged: onChanged, activeColor: colors.primary, activeTrackColor: colors.primary.withOpacity(0.3), inactiveThumbColor: colors.surface, inactiveTrackColor: colors.outline),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDefaultTabSelector() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final labels = ['影视', '阅读', '笔记'];
|
||||
final icons = [Icons.movie_outlined, Icons.menu_book_outlined, Icons.note_outlined];
|
||||
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.home_outlined, color: Color(0xFF666666), size: 22)),
|
||||
title: const Text('默认启动标签', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
||||
subtitle: const Text('打开应用时默认显示的页面', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.home_outlined, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||
title: Text('默认启动标签', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
subtitle: Text('打开应用时默认显示的页面', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(labels[_defaultTabIndex], style: const TextStyle(fontSize: 14, color: Color(0xFF999999))),
|
||||
Text(labels[_defaultTabIndex], style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC), size: 20),
|
||||
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20),
|
||||
],
|
||||
),
|
||||
onTap: () => _showDefaultTabPicker(labels, icons),
|
||||
@@ -775,24 +870,25 @@ class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
|
||||
}
|
||||
|
||||
void _showDefaultTabPicker(List<String> labels, List<IconData> icons) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) => Container(
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
decoration: BoxDecoration(color: colors.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(16))),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(width: 36, height: 4, decoration: BoxDecoration(color: const Color(0xFFDDDDDD), borderRadius: BorderRadius.circular(2))),
|
||||
Container(width: 36, height: 4, decoration: BoxDecoration(color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
|
||||
const SizedBox(height: 20),
|
||||
const Align(alignment: Alignment.centerLeft, child: Padding(padding: EdgeInsets.symmetric(horizontal: 24), child: Text('默认启动标签', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))))),
|
||||
Align(alignment: Alignment.centerLeft, child: Padding(padding: const EdgeInsets.symmetric(horizontal: 24), child: Text('默认启动标签', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)))),
|
||||
const SizedBox(height: 16),
|
||||
for (int i = 0; i < labels.length; i++)
|
||||
ListTile(
|
||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icons[i], color: const Color(0xFF666666))),
|
||||
title: Text(labels[i], style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
||||
trailing: _defaultTabIndex == i ? const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 20) : null,
|
||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icons[i], color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
title: Text(labels[i], style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
trailing: _defaultTabIndex == i ? Icon(Icons.check, color: colors.onSurface, size: 20) : null,
|
||||
onTap: () async { await _userPrefs.setDefaultMainTabIndex(i); setState(() => _defaultTabIndex = i); Navigator.pop(ctx); },
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -828,28 +924,29 @@ class _LayoutSettingsPageState extends State<LayoutSettingsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(title: const Text('布局设置')),
|
||||
body: ListView(
|
||||
children: [
|
||||
_buildSectionHeader('笔记布局'),
|
||||
_buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列列表,简洁清晰', value: 0, groupValue: _noteLayout, onTap: () => _setLayout('note', 0)),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildLayoutOption(icon: Icons.grid_view_outlined, title: '瀑布流布局', subtitle: '双列卡片,图文并茂', value: 1, groupValue: _noteLayout, onTap: () => _setLayout('note', 1)),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildLayoutOption(icon: Icons.timeline_outlined, title: '时间线布局', subtitle: '按时间排列,纵览全局', value: 2, groupValue: _noteLayout, onTap: () => _setLayout('note', 2)),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildSectionHeader('影视布局'),
|
||||
_buildLayoutOption(icon: Icons.grid_view_outlined, title: '海报网格', subtitle: '三列海报,赏心悦目', value: 0, groupValue: _movieLayout, onTap: () => _setLayout('movie', 0)),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列卡片,信息一览', value: 1, groupValue: _movieLayout, onTap: () => _setLayout('movie', 1)),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildSectionHeader('阅读布局'),
|
||||
_buildLayoutOption(icon: Icons.grid_view_outlined, title: '封面网格', subtitle: '三列封面,清新雅致', value: 0, groupValue: _bookLayout, onTap: () => _setLayout('book', 0)),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
_buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列卡片,信息一览', value: 1, groupValue: _bookLayout, onTap: () => _setLayout('book', 1)),
|
||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
||||
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -864,20 +961,22 @@ class _LayoutSettingsPageState extends State<LayoutSettingsPage> {
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12),
|
||||
child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLayoutOption({required IconData icon, required String title, required String subtitle, required int value, required int groupValue, required VoidCallback onTap}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final selected = value == groupValue;
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)),
|
||||
title: Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
||||
subtitle: Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
trailing: selected ? const Icon(Icons.check_circle, color: Color(0xFF1A1A1A), size: 20) : const Icon(Icons.circle_outlined, color: Color(0xFFDDDDDD), size: 20),
|
||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||
title: Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
trailing: selected ? Icon(Icons.check_circle, color: colors.onSurface, size: 20) : Icon(Icons.circle_outlined, color: colors.onSurface.withValues(alpha: 0.15), size: 20),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
@@ -911,10 +1010,11 @@ class _WebViewPageState extends State<WebViewPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(title: const Text(''), actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: () => _controller.reload())]),
|
||||
body: Stack(children: [WebViewWidget(controller: _controller), if (_isLoading) const Center(child: CircularProgressIndicator(color: Color(0xFF999999)))]),
|
||||
body: Stack(children: [WebViewWidget(controller: _controller), if (_isLoading) Center(child: CircularProgressIndicator(color: colors.onSurface.withValues(alpha: 0.4)))]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class _DeletedItem {
|
||||
|
||||
class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
List<_DeletedItem> _allItems = [];
|
||||
_ItemType? _filterType; // null = 全部
|
||||
_ItemType? _filterType;
|
||||
bool _isLoading = true;
|
||||
|
||||
List<_DeletedItem> get _filteredItems =>
|
||||
@@ -80,8 +80,9 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('回收站'),
|
||||
actions: [
|
||||
@@ -91,12 +92,12 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
child: TextButton(
|
||||
onPressed: _showClearAllDialog,
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
foregroundColor: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: const BorderSide(color: Color(0xFFFFDDDD), width: 0.5),
|
||||
side: BorderSide(color: colors.error.withValues(alpha: 0.15), width: 0.5),
|
||||
),
|
||||
minimumSize: Size.zero,
|
||||
),
|
||||
@@ -106,12 +107,10 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
],
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A)))
|
||||
? Center(child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary))
|
||||
: Column(
|
||||
children: [
|
||||
// 筛选标签行
|
||||
if (_allItems.isNotEmpty) _buildFilterRow(),
|
||||
// 列表
|
||||
Expanded(
|
||||
child: _filteredItems.isEmpty
|
||||
? _buildEmptyState()
|
||||
@@ -130,11 +129,12 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
}
|
||||
|
||||
Widget _buildFilterRow() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: Color(0xFFEEEEEE), width: 0.5)),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5)),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
@@ -149,6 +149,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
}
|
||||
|
||||
Widget _filterChip(String label, _ItemType? type) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final active = _filterType == type;
|
||||
final count = type == null ? _allItems.length : _allItems.where((i) => i.type == type).length;
|
||||
return GestureDetector(
|
||||
@@ -156,7 +157,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5),
|
||||
color: active ? colors.primary : colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
@@ -164,7 +165,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: active ? Colors.white : const Color(0xFF888888),
|
||||
color: active ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -172,6 +173,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
}
|
||||
|
||||
Widget _buildCard(_DeletedItem item) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Dismissible(
|
||||
key: Key('${item.type.name}_${item.id}'),
|
||||
direction: DismissDirection.endToStart,
|
||||
@@ -181,9 +183,9 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
@@ -193,11 +195,11 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Icon(item.icon, size: 20, color: const Color(0xFF888888)),
|
||||
child: Icon(item.icon, size: 20, color: colors.onSurface.withValues(alpha: 0.5)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -210,7 +212,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.title,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A), height: 1.3),
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface, height: 1.3),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -219,13 +221,13 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Text(
|
||||
item.typeLabel,
|
||||
style: const TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: Color(0xFF999999)),
|
||||
style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -233,15 +235,15 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
item.subtitle,
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)),
|
||||
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_actionBtn(Icons.restore, '恢复', const Color(0xFF1A1A1A), () => _restore(item)),
|
||||
_actionBtn(Icons.restore, '恢复', colors.primary, () => _restore(item)),
|
||||
const SizedBox(width: 6),
|
||||
_actionBtn(Icons.delete_outline, '删除', Colors.red, () => _permanentDelete(item)),
|
||||
_actionBtn(Icons.delete_outline, '删除', colors.error, () => _permanentDelete(item)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -263,6 +265,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
}
|
||||
|
||||
Widget _actionBtn(IconData icon, String tooltip, Color color, VoidCallback onTap) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
@@ -274,9 +277,9 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Icon(icon, size: 16, color: color),
|
||||
),
|
||||
@@ -286,6 +289,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -294,18 +298,18 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F8F8),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: const Icon(Icons.delete_outline, size: 32, color: Color(0xFFD5D5D5)),
|
||||
child: Icon(Icons.delete_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.15)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_filterType == null ? '回收站是空的' : '没有删除的项目',
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)),
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text('删除的项目会显示在这里', style: TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))),
|
||||
Text('删除的项目会显示在这里', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -344,25 +348,26 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
}
|
||||
|
||||
Future<bool> _showConfirmDialog(String message) async {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
content: Text(message, style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)),
|
||||
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
content: Text(message, style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
style: TextButton.styleFrom(foregroundColor: const Color(0xFF666666)),
|
||||
style: TextButton.styleFrom(foregroundColor: colors.onSurface.withValues(alpha: 0.6)),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colors.error,
|
||||
foregroundColor: colors.onError,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
@@ -377,27 +382,28 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
|
||||
void _showClearAllDialog() {
|
||||
final pageContext = context;
|
||||
final colors = Theme.of(pageContext).colorScheme;
|
||||
showDialog(
|
||||
context: pageContext,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Row(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
|
||||
SizedBox(width: 8),
|
||||
Text('清空回收站', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
Text('清空回收站', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
],
|
||||
),
|
||||
content: const Text(
|
||||
content: Text(
|
||||
'确定要清空回收站吗?所有项目将被彻底删除,此操作不可恢复。',
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5),
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
style: TextButton.styleFrom(foregroundColor: const Color(0xFF666666)),
|
||||
style: TextButton.styleFrom(foregroundColor: colors.onSurface.withValues(alpha: 0.6)),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
@@ -408,8 +414,8 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||
if (mounted) ToastUtil.show(pageContext, '回收站已清空');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colors.error,
|
||||
foregroundColor: colors.onError,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
|
||||
@@ -100,8 +100,9 @@ class _SearchPageState extends State<SearchPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('搜索'),
|
||||
elevation: 0,
|
||||
@@ -121,18 +122,19 @@ class _SearchPageState extends State<SearchPage> {
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 4),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
focusNode: _focusNode,
|
||||
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
|
||||
style: TextStyle(fontSize: 15, color: colors.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索标题、别名、内容...',
|
||||
hintStyle: const TextStyle(color: Color(0xFFB0B0B0), fontSize: 15),
|
||||
prefixIcon: const Padding(
|
||||
padding: EdgeInsets.only(left: 12, right: 8),
|
||||
child: Icon(Icons.search, color: Color(0xFF1A1A1A), size: 22),
|
||||
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.35), fontSize: 15),
|
||||
prefixIcon: Padding(
|
||||
padding: const EdgeInsets.only(left: 12, right: 8),
|
||||
child: Icon(Icons.search, color: colors.onSurface, size: 22),
|
||||
),
|
||||
prefixIconConstraints: const BoxConstraints(minWidth: 42, minHeight: 42),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
@@ -147,15 +149,15 @@ class _SearchPageState extends State<SearchPage> {
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE5E5E5),
|
||||
color: colors.outline,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: const Icon(Icons.close, color: Color(0xFF666666), size: 16),
|
||||
child: Icon(Icons.close, color: colors.onSurface.withValues(alpha: 0.6), size: 16),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFF8F8F8),
|
||||
fillColor: colors.surfaceContainerHigh,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: BorderSide.none,
|
||||
@@ -166,7 +168,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1.5),
|
||||
borderSide: BorderSide(color: colors.primary, width: 1.5),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
@@ -205,26 +207,27 @@ class _SearchPageState extends State<SearchPage> {
|
||||
}
|
||||
|
||||
Widget _buildTypeChip(String label, IconData icon, bool selected, ValueChanged<bool> onChanged) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: () => onChanged(!selected),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5),
|
||||
color: selected ? colors.primary : colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: selected ? Colors.white : const Color(0xFF888888)),
|
||||
Icon(icon, size: 14, color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.5)),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: selected ? Colors.white : const Color(0xFF888888),
|
||||
color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -234,6 +237,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
}
|
||||
|
||||
Widget _buildInitialState() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -242,21 +246,22 @@ class _SearchPageState extends State<SearchPage> {
|
||||
width: 88,
|
||||
height: 88,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F8F8),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: const Icon(Icons.search_rounded, size: 44, color: Color(0xFFD0D0D0)),
|
||||
child: Icon(Icons.search_rounded, size: 44, color: colors.onSurface.withValues(alpha: 0.2)),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text('输入关键词搜索', style: TextStyle(fontSize: 15, color: Color(0xFF999999))),
|
||||
Text('输入关键词搜索', style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const SizedBox(height: 6),
|
||||
const Text('可同时筛选影视、书籍、笔记', style: TextStyle(fontSize: 13, color: Color(0xFFCCCCCC))),
|
||||
Text('可同时筛选影视、书籍、笔记', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -265,15 +270,15 @@ class _SearchPageState extends State<SearchPage> {
|
||||
width: 88,
|
||||
height: 88,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F8F8),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: const Icon(Icons.search_off_rounded, size: 44, color: Color(0xFFD0D0D0)),
|
||||
child: Icon(Icons.search_off_rounded, size: 44, color: colors.onSurface.withValues(alpha: 0.2)),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text('未找到相关内容', style: TextStyle(fontSize: 15, color: Color(0xFF999999))),
|
||||
Text('未找到相关内容', style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const SizedBox(height: 6),
|
||||
const Text('换个关键词试试', style: TextStyle(fontSize: 13, color: Color(0xFFCCCCCC))),
|
||||
Text('换个关键词试试', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -302,13 +307,14 @@ class _SearchPageState extends State<SearchPage> {
|
||||
// ─── 影视结果项 ──────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildMovieItem(Movie movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => MovieDetailPage(movie: movie))),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Row(
|
||||
@@ -328,17 +334,17 @@ class _SearchPageState extends State<SearchPage> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(movie.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
if (movie.alternateTitles.isNotEmpty) ...[
|
||||
const SizedBox(height: 3),
|
||||
Text(movie.alternateTitles.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
|
||||
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
|
||||
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -348,13 +354,14 @@ class _SearchPageState extends State<SearchPage> {
|
||||
// ─── 书籍结果项 ──────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildBookItem(Book book) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => BookDetailPage(book: book))),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Row(
|
||||
@@ -374,17 +381,17 @@ class _SearchPageState extends State<SearchPage> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(book.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
if (book.authors.isNotEmpty) ...[
|
||||
const SizedBox(height: 3),
|
||||
Text(book.authors.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
|
||||
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
|
||||
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -394,13 +401,14 @@ class _SearchPageState extends State<SearchPage> {
|
||||
// ─── 笔记结果项 ──────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildNoteItem(Note note) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => NoteDetailPage(note: note))),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Column(
|
||||
@@ -410,7 +418,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
children: [
|
||||
_typeBadge('笔记', const Color(0xFF66BB6A)),
|
||||
const Spacer(),
|
||||
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
|
||||
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
@@ -418,7 +426,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
note.summary.trim().isEmpty ? '(无内容)' : note.summary.trim(),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF333333), height: 1.6),
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75), height: 1.6),
|
||||
),
|
||||
if (note.tags.isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
@@ -428,10 +436,10 @@ class _SearchPageState extends State<SearchPage> {
|
||||
children: note.tags.map((tag) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(tag, style: const TextStyle(fontSize: 11, color: Color(0xFF888888))),
|
||||
child: Text(tag, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||
)).toList(),
|
||||
),
|
||||
],
|
||||
@@ -444,18 +452,19 @@ class _SearchPageState extends State<SearchPage> {
|
||||
// ─── 通用组件 ────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildPosterThumb(String? path, IconData fallback) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
width: 48,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F0F0),
|
||||
color: colors.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: path != null && path.isNotEmpty
|
||||
? Image.file(File(path), fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Icon(fallback, size: 22, color: const Color(0xFFCCCCCC)))
|
||||
: Icon(fallback, size: 22, color: const Color(0xFFCCCCCC)),
|
||||
errorBuilder: (_, __, ___) => Icon(fallback, size: 22, color: colors.onSurface.withValues(alpha: 0.25)))
|
||||
: Icon(fallback, size: 22, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -471,11 +480,12 @@ class _SearchPageState extends State<SearchPage> {
|
||||
}
|
||||
|
||||
Widget _statusBadge(String status) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final (label, bg, fg) = switch (status) {
|
||||
'watched' => ('已看', const Color(0xFF1A1A1A), Colors.white),
|
||||
'watching' => ('在看', const Color(0xFFF0F0F0), const Color(0xFF666666)),
|
||||
'want_to_watch' => ('想看', const Color(0xFFF5F5F5), const Color(0xFF999999)),
|
||||
_ => ('', const Color(0xFFF5F5F5), const Color(0xFFBBBBBB)),
|
||||
'watched' => ('已看', colors.primary, colors.onPrimary),
|
||||
'watching' => ('在看', colors.outlineVariant, colors.onSurface.withValues(alpha: 0.6)),
|
||||
'want_to_watch' => ('想看', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.4)),
|
||||
_ => ('', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.3)),
|
||||
};
|
||||
if (label.isEmpty) return const SizedBox.shrink();
|
||||
return Container(
|
||||
@@ -486,11 +496,12 @@ class _SearchPageState extends State<SearchPage> {
|
||||
}
|
||||
|
||||
Widget _bookStatusBadge(String status) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final (label, bg, fg) = switch (status) {
|
||||
'read' => ('已读', const Color(0xFF1A1A1A), Colors.white),
|
||||
'reading' => ('在读', const Color(0xFFF0F0F0), const Color(0xFF666666)),
|
||||
'want_to_read' => ('想读', const Color(0xFFF5F5F5), const Color(0xFF999999)),
|
||||
_ => ('', const Color(0xFFF5F5F5), const Color(0xFFBBBBBB)),
|
||||
'read' => ('已读', colors.primary, colors.onPrimary),
|
||||
'reading' => ('在读', colors.outlineVariant, colors.onSurface.withValues(alpha: 0.6)),
|
||||
'want_to_read' => ('想读', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.4)),
|
||||
_ => ('', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.3)),
|
||||
};
|
||||
if (label.isEmpty) return const SizedBox.shrink();
|
||||
return Container(
|
||||
|
||||
@@ -19,8 +19,9 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(title: const Text('数据统计')),
|
||||
body: Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
@@ -63,6 +64,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
// ─── 总览卡片 ────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildTotalCards(List<Movie> movies, List<Book> books, List<Note> notes) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final items = <_CardData>[];
|
||||
if (_showMovies) items.add(_CardData('影视', movies.length, Icons.movie_outlined, const Color(0xFF4A90D9)));
|
||||
if (_showBooks) items.add(_CardData('书籍', books.length, Icons.menu_book_outlined, const Color(0xFF7E57C2)));
|
||||
@@ -83,7 +85,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
const SizedBox(height: 10),
|
||||
Text('${d.count}', style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700, color: d.color)),
|
||||
const SizedBox(height: 2),
|
||||
Text(d.label, style: const TextStyle(fontSize: 12, color: Color(0xFF888888))),
|
||||
Text(d.label, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -94,6 +96,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
// ─── 状态分布 ────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildStatusSection(String title, List items, String Function(dynamic) getStatus, Map<String, String> labels) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final active = items.where((i) => !(i is Movie) || !i.isDeleted).toList();
|
||||
final total = active.length;
|
||||
|
||||
@@ -110,11 +113,11 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(e.key, style: const TextStyle(fontSize: 13, color: Color(0xFF666666))),
|
||||
Text(e.key, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
const Spacer(),
|
||||
Text('$count', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text('$count', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
const SizedBox(width: 4),
|
||||
Text('${(pct * 100).toStringAsFixed(0)}%', style: const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))),
|
||||
Text('${(pct * 100).toStringAsFixed(0)}%', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
@@ -122,8 +125,8 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
child: LinearProgressIndicator(
|
||||
value: pct,
|
||||
backgroundColor: const Color(0xFFF0F0F0),
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: colors.outlineVariant,
|
||||
color: colors.primary,
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
@@ -138,6 +141,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
// ─── 类型/标签分布 ───────────────────────────────────────────────────
|
||||
|
||||
Widget _buildGenreDistribution(String title, List items) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final genreCounts = <String, int>{};
|
||||
for (final item in items) {
|
||||
for (final genre in (item.genres as List<String>)) {
|
||||
@@ -161,17 +165,17 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Text(e.key, style: const TextStyle(fontSize: 12, color: Color(0xFF666666)), overflow: TextOverflow.ellipsis),
|
||||
child: Text(e.key, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.6)), overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: LinearProgressIndicator(value: pct, backgroundColor: const Color(0xFFF0F0F0), color: const Color(0xFF1A1A1A), minHeight: 4),
|
||||
child: LinearProgressIndicator(value: pct, backgroundColor: colors.outlineVariant, color: colors.primary, minHeight: 4),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text('${e.value}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text('${e.value}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -181,6 +185,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
}
|
||||
|
||||
Widget _buildNoteTagDistribution(String title, List<Note> notes) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final tagCounts = <String, int>{};
|
||||
for (final note in notes) {
|
||||
for (final tag in note.tags) {
|
||||
@@ -204,17 +209,17 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Text(e.key, style: const TextStyle(fontSize: 12, color: Color(0xFF666666)), overflow: TextOverflow.ellipsis),
|
||||
child: Text(e.key, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.6)), overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: LinearProgressIndicator(value: pct, backgroundColor: const Color(0xFFF0F0F0), color: const Color(0xFF1A1A1A), minHeight: 4),
|
||||
child: LinearProgressIndicator(value: pct, backgroundColor: colors.outlineVariant, color: colors.primary, minHeight: 4),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text('${e.value}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text('${e.value}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -226,6 +231,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
// ─── 评分分布 ────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildRatingDistribution(String title, List<Movie> movies, List<Book> books) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final allRatings = <double>[];
|
||||
for (final m in movies) {
|
||||
if (m.rating != null) allRatings.add(m.rating!);
|
||||
@@ -255,10 +261,10 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text('平均评分', style: TextStyle(fontSize: 13, color: Color(0xFF666666))),
|
||||
Text('平均评分', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
const Spacer(),
|
||||
Text(avg.toStringAsFixed(1), style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A))),
|
||||
const Text(' / 10', style: TextStyle(fontSize: 13, color: Color(0xFFBBBBBB))),
|
||||
Text(avg.toStringAsFixed(1), style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: colors.onSurface)),
|
||||
Text(' / 10', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -268,7 +274,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 28,
|
||||
child: Text(ranges[i], style: const TextStyle(fontSize: 11, color: Color(0xFF999999))),
|
||||
child: Text(ranges[i], style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
@@ -276,14 +282,14 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: LinearProgressIndicator(
|
||||
value: maxCount > 0 ? counts[i] / maxCount : 0.0,
|
||||
backgroundColor: const Color(0xFFF0F0F0),
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: colors.outlineVariant,
|
||||
color: colors.primary,
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text('${counts[i]}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text('${counts[i]}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
],
|
||||
),
|
||||
)),
|
||||
@@ -295,6 +301,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
// ─── 月度趋势 ────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildMonthlyTrend(List<Movie> movies, List<Book> books, List<Note> notes) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final now = DateTime.now();
|
||||
final months = List.generate(6, (i) {
|
||||
final d = DateTime(now.year, now.month - (5 - i), 1);
|
||||
@@ -323,7 +330,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
final maxVal = allValues.isEmpty ? 1 : allValues.reduce((a, b) => a > b ? a : b);
|
||||
final safeMax = maxVal == 0 ? 1 : maxVal;
|
||||
|
||||
final colors = { '影视': const Color(0xFF4A90D9), '书籍': const Color(0xFF7E57C2), '笔记': const Color(0xFF66BB6A) };
|
||||
final brandColors = { '影视': const Color(0xFF4A90D9), '书籍': const Color(0xFF7E57C2), '笔记': const Color(0xFF66BB6A) };
|
||||
|
||||
return _buildCard(
|
||||
title: '近6月趋势',
|
||||
@@ -345,7 +352,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
height: h < 2 && e.value[i] > 0 ? 2 : h,
|
||||
margin: const EdgeInsets.only(top: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: colors[e.key]!.withValues(alpha: 0.7),
|
||||
color: brandColors[e.key]!.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
);
|
||||
@@ -357,12 +364,12 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(height: 1, color: Color(0xFFF0F0F0)),
|
||||
Divider(height: 1, color: colors.outlineVariant),
|
||||
const SizedBox(height: 8),
|
||||
// 月份标签
|
||||
Row(
|
||||
children: months.map((m) => Expanded(
|
||||
child: Text(m, textAlign: TextAlign.center, style: const TextStyle(fontSize: 11, color: Color(0xFFBBBBBB))),
|
||||
child: Text(m, textAlign: TextAlign.center, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||
)).toList(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -374,9 +381,9 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(width: 8, height: 8, decoration: BoxDecoration(color: colors[k], borderRadius: BorderRadius.circular(2))),
|
||||
Container(width: 8, height: 8, decoration: BoxDecoration(color: brandColors[k], borderRadius: BorderRadius.circular(2))),
|
||||
const SizedBox(width: 4),
|
||||
Text(k, style: const TextStyle(fontSize: 12, color: Color(0xFF888888))),
|
||||
Text(k, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||
],
|
||||
),
|
||||
)).toList(),
|
||||
@@ -394,10 +401,11 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
// ─── 通用卡片 ────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildCard({required String title, required Widget child}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Column(
|
||||
@@ -405,9 +413,9 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(width: 3, height: 14, decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(2))),
|
||||
Container(width: 3, height: 14, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(2))),
|
||||
const SizedBox(width: 8),
|
||||
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -153,8 +153,9 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('漫步'),
|
||||
actions: [
|
||||
@@ -166,15 +167,15 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.casino_outlined, size: 14, color: Colors.white),
|
||||
SizedBox(width: 5),
|
||||
Text('随机', style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.w500)),
|
||||
Icon(Icons.casino_outlined, size: 14, color: colors.onPrimary),
|
||||
const SizedBox(width: 5),
|
||||
Text('随机', style: TextStyle(fontSize: 12, color: colors.onPrimary, fontWeight: FontWeight.w500)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -183,8 +184,10 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
||||
],
|
||||
),
|
||||
body: _currentItem == null
|
||||
? const Center(child: Text('还没有任何内容\n去添加一些吧', textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 15, color: Color(0xFFBBBBBB), height: 1.6)))
|
||||
? Center(
|
||||
child: Text('还没有任何内容\n去添加一些吧',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.3), height: 1.6)))
|
||||
: Consumer<AppProvider>(builder: (context, provider, _) {
|
||||
final item = _currentItem!;
|
||||
final hasImage = item.imagePath != null && item.imagePath!.isNotEmpty && File(item.imagePath!).existsSync();
|
||||
@@ -216,7 +219,7 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
||||
child: Text(
|
||||
item.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A), height: 1.3),
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.3),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -225,7 +228,7 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(item.subtitle, textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 13, color: Color(0xFF999999))),
|
||||
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -242,18 +245,18 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(item.detail,
|
||||
style: const TextStyle(fontSize: 13, color: Color(0xFF777777), height: 1.7)),
|
||||
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6), height: 1.7)),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
// 时间
|
||||
const SizedBox(height: 12),
|
||||
Text(_actionText(item), style: const TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))),
|
||||
Text(_actionText(item), style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
@@ -302,11 +305,12 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
||||
}
|
||||
|
||||
Widget _buildNoteCard(_StrollItem item, bool hasImage) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 360),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.06), blurRadius: 16, offset: const Offset(0, 6)),
|
||||
@@ -322,12 +326,12 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Text(item.detail.isEmpty ? '(无内容)' : item.detail,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF444444), height: 1.8)),
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.73), height: 1.8)),
|
||||
),
|
||||
const Divider(height: 1, color: Color(0xFFF0F0F0)),
|
||||
Divider(height: 1, color: colors.outlineVariant),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
child: Text('${item.detail.length} 字 · Mooknote', style: const TextStyle(fontSize: 11, color: Color(0xFFBBBBBB))),
|
||||
child: Text('${item.detail.length} 字 · Mooknote', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -335,8 +339,9 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
||||
}
|
||||
|
||||
Widget _buildPlaceholder(_StrollItem item) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
color: const Color(0xFFF8F8F8),
|
||||
color: colors.surfaceContainerHigh,
|
||||
child: Center(
|
||||
child: Icon(item.icon, size: 48, color: item.color.withValues(alpha: 0.2)),
|
||||
),
|
||||
|
||||
@@ -40,71 +40,74 @@ class _BackupPageState extends State<BackupPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('本地备份'),
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
// 自动备份开关 - 紧凑一行
|
||||
_buildAutoBackupSection(),
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
// 自动备份开关 - 紧凑一行
|
||||
_buildAutoBackupSection(colors),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 手动备份
|
||||
_buildSectionTitle('手动备份'),
|
||||
const SizedBox(height: 12),
|
||||
_buildActionCard(
|
||||
title: '导出数据',
|
||||
description: '将所有数据导出为 zip 文件,可用于备份或迁移到其他设备',
|
||||
icon: Icons.upload_outlined,
|
||||
buttonText: '导出',
|
||||
isLoading: _isExporting,
|
||||
onTap: _exportData,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildActionCard(
|
||||
title: '导入数据',
|
||||
description: '从备份文件导入数据,将覆盖当前所有数据',
|
||||
icon: Icons.download_outlined,
|
||||
buttonText: '导入',
|
||||
isLoading: _isImporting,
|
||||
onTap: _importData,
|
||||
isDestructive: true,
|
||||
),
|
||||
// 手动备份
|
||||
_buildSectionTitle(colors, '手动备份'),
|
||||
const SizedBox(height: 12),
|
||||
_buildActionCard(
|
||||
colors: colors,
|
||||
title: '导出数据',
|
||||
description: '将所有数据导出为 zip 文件,可用于备份或迁移到其他设备',
|
||||
icon: Icons.upload_outlined,
|
||||
buttonText: '导出',
|
||||
isLoading: _isExporting,
|
||||
onTap: _exportData,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildActionCard(
|
||||
colors: colors,
|
||||
title: '导入数据',
|
||||
description: '从备份文件导入数据,将覆盖当前所有数据',
|
||||
icon: Icons.download_outlined,
|
||||
buttonText: '导入',
|
||||
isLoading: _isImporting,
|
||||
onTap: _importData,
|
||||
isDestructive: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 使用说明
|
||||
_buildInfoSection(),
|
||||
],
|
||||
),
|
||||
// 使用说明
|
||||
_buildInfoSection(colors),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建区块标题
|
||||
Widget _buildSectionTitle(String title) {
|
||||
Widget _buildSectionTitle(ColorScheme colors, String title) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 4,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -113,6 +116,7 @@ class _BackupPageState extends State<BackupPage> {
|
||||
|
||||
/// 构建操作卡片
|
||||
Widget _buildActionCard({
|
||||
required ColorScheme colors,
|
||||
required String title,
|
||||
required String description,
|
||||
required IconData icon,
|
||||
@@ -124,7 +128,7 @@ class _BackupPageState extends State<BackupPage> {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
@@ -136,19 +140,17 @@ class _BackupPageState extends State<BackupPage> {
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isDestructive
|
||||
? Colors.red.withOpacity(0.3)
|
||||
: const Color(0xFFE8E8E8),
|
||||
color: isDestructive ? Colors.red.withOpacity(0.3) : colors.outline,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 22,
|
||||
color: isDestructive ? Colors.red : const Color(0xFF666666),
|
||||
color: isDestructive ? Colors.red : colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@@ -158,18 +160,18 @@ class _BackupPageState extends State<BackupPage> {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
description,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
@@ -185,25 +187,25 @@ class _BackupPageState extends State<BackupPage> {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: isLoading ? const Color(0xFFCCCCCC) : const Color(0xFF1A1A1A),
|
||||
color: isLoading ? colors.onSurface.withValues(alpha: 0.25) : colors.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation(Colors.white),
|
||||
valueColor: AlwaysStoppedAnimation(colors.onPrimary),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
buttonText,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
color: colors.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -215,11 +217,11 @@ class _BackupPageState extends State<BackupPage> {
|
||||
}
|
||||
|
||||
/// 构建信息说明区域
|
||||
Widget _buildInfoSection() {
|
||||
Widget _buildInfoSection(ColorScheme colors) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F8F8),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
@@ -231,42 +233,42 @@ class _BackupPageState extends State<BackupPage> {
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outline, width: 0.5),
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.info_outline,
|
||||
size: 18,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
Text(
|
||||
'使用说明',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItem('1', '导出数据会生成一个 .zip 文件,包含所有数据和图片'),
|
||||
_buildInfoItem(colors, '1', '导出数据会生成一个 .zip 文件,包含所有数据和图片'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoItem('2', '选择保存路径后,可以通过微信、邮件等方式发送备份文件'),
|
||||
_buildInfoItem(colors, '2', '选择保存路径后,可以通过微信、邮件等方式发送备份文件'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoItem('3', '在新设备上选择导入数据,选择备份文件即可恢复'),
|
||||
_buildInfoItem(colors, '3', '在新设备上选择导入数据,选择备份文件即可恢复'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoItem('4', '导入数据会完全覆盖当前设备的数据,请谨慎操作'),
|
||||
_buildInfoItem(colors, '4', '导入数据会完全覆盖当前设备的数据,请谨慎操作'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建信息项
|
||||
Widget _buildInfoItem(String number, String text) {
|
||||
Widget _buildInfoItem(ColorScheme colors, String number, String text) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -274,16 +276,16 @@ class _BackupPageState extends State<BackupPage> {
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE8E8E8),
|
||||
color: colors.outline,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
number,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -292,9 +294,9 @@ class _BackupPageState extends State<BackupPage> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -311,85 +313,93 @@ class _BackupPageState extends State<BackupPage> {
|
||||
}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 24),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(title, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
],
|
||||
),
|
||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
content,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (detail != null && detail.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F8F8),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
detail,
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(Icons.check, color: colors.primary, size: 24),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(title,
|
||||
style: TextStyle(
|
||||
fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
],
|
||||
],
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: const Size(120, 40),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: const Text('确定', style: TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))),
|
||||
),
|
||||
],
|
||||
),
|
||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
content,
|
||||
style: TextStyle(
|
||||
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (detail != null && detail.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
detail,
|
||||
style:
|
||||
TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: const Size(120, 40),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: Text('确定', style: TextStyle(fontSize: 14, color: colors.primary)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 导出数据
|
||||
Future<void> _exportData() async {
|
||||
setState(() => _isExporting = true);
|
||||
|
||||
|
||||
try {
|
||||
final result = await BackupService.instance.exportDataWithImages();
|
||||
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
|
||||
if (result.cancelled) {
|
||||
ToastUtil.show(context, '已取消导出');
|
||||
} else if (result.success) {
|
||||
_showSuccessDialog(
|
||||
title: '导出成功',
|
||||
content: '备份文件已保存,包含:\n影视 ${result.movieCount} · 书籍 ${result.bookCount} · 笔记 ${result.noteCount} · 图片 ${result.imageCount}',
|
||||
content:
|
||||
'备份文件已保存,包含:\n影视 ${result.movieCount} · 书籍 ${result.bookCount} · 笔记 ${result.noteCount} · 图片 ${result.imageCount}',
|
||||
detail: result.filePath ?? '',
|
||||
);
|
||||
} else {
|
||||
@@ -411,54 +421,63 @@ class _BackupPageState extends State<BackupPage> {
|
||||
// 显示确认对话框
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
|
||||
),
|
||||
child: const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
|
||||
const SizedBox(width: 12),
|
||||
Text('确认导入',
|
||||
style: TextStyle(
|
||||
fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
],
|
||||
),
|
||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Text(
|
||||
'导入数据将覆盖当前所有数据,此操作不可恢复。',
|
||||
style: TextStyle(
|
||||
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6),
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: Text('取消',
|
||||
style: TextStyle(
|
||||
color: colors.onSurface.withValues(alpha: 0.6), fontSize: 14)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: const Text('确认导入',
|
||||
style: TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text('确认导入', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
],
|
||||
),
|
||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
content: const Padding(
|
||||
padding: EdgeInsets.only(top: 16),
|
||||
child: Text(
|
||||
'导入数据将覆盖当前所有数据,此操作不可恢复。',
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6),
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666), fontSize: 14)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: const Text('确认导入', style: TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
@@ -467,7 +486,7 @@ class _BackupPageState extends State<BackupPage> {
|
||||
|
||||
try {
|
||||
final result = await BackupService.instance.importData();
|
||||
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (result.cancelled) {
|
||||
@@ -499,13 +518,13 @@ class _BackupPageState extends State<BackupPage> {
|
||||
}
|
||||
|
||||
/// 构建自动备份区域 - 紧凑一行
|
||||
Widget _buildAutoBackupSection() {
|
||||
Widget _buildAutoBackupSection(ColorScheme colors) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outline, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -513,11 +532,12 @@ class _BackupPageState extends State<BackupPage> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outline, width: 0.5),
|
||||
),
|
||||
child: const Icon(Icons.schedule, size: 20, color: Color(0xFF666666)),
|
||||
child: Icon(Icons.schedule,
|
||||
size: 20, color: colors.onSurface.withValues(alpha: 0.6)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -525,22 +545,25 @@ class _BackupPageState extends State<BackupPage> {
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
Text(
|
||||
'自动本地备份',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
||||
style: TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_backupDirPath!,
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
|
||||
style:
|
||||
TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
)
|
||||
: const Text(
|
||||
: Text(
|
||||
'自动本地备份',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
||||
style: TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
@@ -555,14 +578,13 @@ class _BackupPageState extends State<BackupPage> {
|
||||
}
|
||||
await _loadAutoBackupStatus();
|
||||
},
|
||||
activeColor: const Color(0xFF1A1A1A),
|
||||
activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3),
|
||||
inactiveThumbColor: Colors.white,
|
||||
inactiveTrackColor: const Color(0xFFE5E5E5),
|
||||
activeThumbColor: colors.primary,
|
||||
activeTrackColor: colors.primary.withValues(alpha: 0.3),
|
||||
inactiveThumbColor: colors.surface,
|
||||
inactiveTrackColor: colors.outline,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,95 +8,163 @@ class CloudSyncPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F8F8),
|
||||
backgroundColor: colors.surfaceContainerHigh,
|
||||
appBar: AppBar(title: const Text('云备份')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
_buildSectionTitle('选择备份方式'),
|
||||
_buildSectionTitle(colors, '选择备份方式'),
|
||||
const SizedBox(height: 12),
|
||||
_buildOption(
|
||||
colors: colors,
|
||||
icon: Icons.storage_outlined,
|
||||
title: 'WebDAV 备份',
|
||||
subtitle: '通过 WebDAV 协议备份到个人云盘',
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())),
|
||||
onTap: () =>
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildOption(
|
||||
colors: colors,
|
||||
icon: Icons.sync_outlined,
|
||||
title: '服务端实时同步',
|
||||
subtitle: '自建服务端,多设备数据实时同步',
|
||||
enabled: false,
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ServerSyncPage())),
|
||||
onTap: () =>
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const ServerSyncPage())),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
_buildInfo(),
|
||||
_buildInfo(colors),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
Widget _buildSectionTitle(ColorScheme colors, String title) {
|
||||
return Row(children: [
|
||||
Container(width: 3, height: 14, decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(2))),
|
||||
Container(
|
||||
width: 3,
|
||||
height: 14,
|
||||
decoration:
|
||||
BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(2))),
|
||||
const SizedBox(width: 8),
|
||||
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text(title,
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildOption({required IconData icon, required String title, required String subtitle, required VoidCallback onTap, bool enabled = true}) {
|
||||
Widget _buildOption({
|
||||
required ColorScheme colors,
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required VoidCallback onTap,
|
||||
bool enabled = true,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: enabled ? onTap : null,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: enabled ? Colors.white : const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))],
|
||||
color: enabled ? colors.surface : colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.03),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
child: Row(children: [
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: enabled ? const Color(0xFF666666) : const Color(0xFFBBBBBB), size: 22)),
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(icon,
|
||||
color: enabled
|
||||
? colors.onSurface.withValues(alpha: 0.6)
|
||||
: colors.onSurface.withValues(alpha: 0.3),
|
||||
size: 22)),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: enabled ? const Color(0xFF1A1A1A) : const Color(0xFFBBBBBB))),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(title,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: enabled ? colors.onSurface : colors.onSurface.withValues(alpha: 0.3))),
|
||||
const SizedBox(height: 3),
|
||||
Text(subtitle, style: TextStyle(fontSize: 12, color: enabled ? const Color(0xFF999999) : const Color(0xFFCCCCCC))),
|
||||
Text(subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: enabled
|
||||
? colors.onSurface.withValues(alpha: 0.4)
|
||||
: colors.onSurface.withValues(alpha: 0.25))),
|
||||
])),
|
||||
Icon(Icons.chevron_right, color: enabled ? const Color(0xFFCCCCCC) : const Color(0xFFE5E5E5)),
|
||||
Icon(Icons.chevron_right,
|
||||
color: enabled
|
||||
? colors.onSurface.withValues(alpha: 0.25)
|
||||
: colors.outline),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfo() {
|
||||
Widget _buildInfo(ColorScheme colors) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))],
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.03),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Container(width: 36, height: 36, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(8)), child: const Icon(Icons.info_outline, size: 18, color: Color(0xFF666666))),
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(8)),
|
||||
child: Icon(Icons.info_outline,
|
||||
size: 18, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
const SizedBox(width: 10),
|
||||
const Text('使用说明', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text('使用说明',
|
||||
style: TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
]),
|
||||
const SizedBox(height: 14),
|
||||
_infoItem('WebDAV 备份:将数据备份到支持 WebDAV 的云盘'),
|
||||
_infoItem(colors, 'WebDAV 备份:将数据备份到支持 WebDAV 的云盘'),
|
||||
const SizedBox(height: 8),
|
||||
_infoItem('服务端实时同步:通过自建服务端实现多设备实时同步'),
|
||||
_infoItem(colors, '服务端实时同步:通过自建服务端实现多设备实时同步'),
|
||||
const SizedBox(height: 8),
|
||||
_infoItem('激活码由服务端管理员在管理后台生成'),
|
||||
_infoItem(colors, '激活码由服务端管理员在管理后台生成'),
|
||||
const SizedBox(height: 8),
|
||||
_infoItem('建议定期备份 + 实时同步配合使用'),
|
||||
_infoItem(colors, '建议定期备份 + 实时同步配合使用'),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoItem(String text) {
|
||||
Widget _infoItem(ColorScheme colors, String text) {
|
||||
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Container(width: 5, height: 5, margin: const EdgeInsets.only(top: 5), decoration: BoxDecoration(color: const Color(0xFFBBBBBB), shape: BoxShape.circle)),
|
||||
Container(
|
||||
width: 5,
|
||||
height: 5,
|
||||
margin: const EdgeInsets.only(top: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.onSurface.withValues(alpha: 0.3), shape: BoxShape.circle)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(text, style: const TextStyle(fontSize: 12, color: Color(0xFF888888), height: 1.5))),
|
||||
Expanded(
|
||||
child: Text(text,
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5), height: 1.5))),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,16 +157,24 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
|
||||
Future<void> _disconnect() async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
title: const Text('断开连接', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
content: const Text('将清除服务器配置和激活信息,确定要断开吗?', style: TextStyle(fontSize: 14, color: Color(0xFF666666))),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定', style: TextStyle(color: Color(0xFFE53935)))),
|
||||
],
|
||||
),
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
title: const Text('断开连接', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
content: Text('将清除服务器配置和激活信息,确定要断开吗?',
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('确定', style: TextStyle(color: Color(0xFFE53935)))),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (confirm != true) return;
|
||||
|
||||
@@ -190,63 +198,107 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F8F8),
|
||||
backgroundColor: colors.surfaceContainerHigh,
|
||||
appBar: AppBar(title: const Text('服务端实时同步')),
|
||||
body: ListView(padding: const EdgeInsets.all(20), children: [
|
||||
// 状态卡片
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))],
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))
|
||||
],
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Container(width: 10, height: 10, decoration: BoxDecoration(
|
||||
color: _isActivated ? const Color(0xFF66BB6A) : const Color(0xFFDDDDDD), shape: BoxShape.circle)),
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: _isActivated
|
||||
? const Color(0xFF66BB6A)
|
||||
: colors.onSurface.withValues(alpha: 0.15),
|
||||
shape: BoxShape.circle)),
|
||||
const SizedBox(width: 10),
|
||||
Text(_isActivated ? '已激活' : '未激活',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600,
|
||||
color: _isActivated ? const Color(0xFF66BB6A) : const Color(0xFFBBBBBB))),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _isActivated
|
||||
? const Color(0xFF66BB6A)
|
||||
: colors.onSurface.withValues(alpha: 0.3))),
|
||||
const Spacer(),
|
||||
if (_isActivated)
|
||||
GestureDetector(
|
||||
onTap: _disconnect,
|
||||
child: Container(padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(6)),
|
||||
child: const Text('断开', style: TextStyle(fontSize: 12, color: Color(0xFFE57373)))),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(6)),
|
||||
child: const Text('断开',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFFE57373)))),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
const Text('服务器地址', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
Text('服务器地址',
|
||||
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const SizedBox(height: 6),
|
||||
TextField(controller: _urlController, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
||||
decoration: _inputDeco('例: http://192.168.1.100:5000')),
|
||||
TextField(
|
||||
controller: _urlController,
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||
decoration: _inputDeco(colors, '例: http://192.168.1.100:5000'),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Text('激活码', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
Text('激活码',
|
||||
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const SizedBox(height: 6),
|
||||
TextField(controller: _codeController, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
||||
textCapitalization: TextCapitalization.characters, decoration: _inputDeco('例: MK-A1B2C3D4E5F6')),
|
||||
TextField(
|
||||
controller: _codeController,
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
decoration: _inputDeco(colors, '例: MK-A1B2C3D4E5F6')),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(width: double.infinity,
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isChecking ? null : _checkActivation,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF1A1A1A), foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: const Color(0xFFDDDDDD), elevation: 0,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
disabledBackgroundColor: colors.onSurface.withValues(alpha: 0.15),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 13)),
|
||||
child: _isChecking
|
||||
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: Text(_isActivated ? '重新验证' : '验证激活', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: colors.onPrimary))
|
||||
: Text(_isActivated ? '重新验证' : '验证激活',
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
),
|
||||
if (_expiresText.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Center(child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.access_time, size: 14, color: _prefs.syncIsPermanent ? const Color(0xFF66BB6A) : const Color(0xFFFF9800)),
|
||||
Center(
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.access_time,
|
||||
size: 14,
|
||||
color: _prefs.syncIsPermanent
|
||||
? const Color(0xFF66BB6A)
|
||||
: const Color(0xFFFF9800)),
|
||||
const SizedBox(width: 6),
|
||||
Text(_expiresText, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500,
|
||||
color: _prefs.syncIsPermanent ? const Color(0xFF66BB6A) : const Color(0xFFFF9800))),
|
||||
Text(_expiresText,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _prefs.syncIsPermanent
|
||||
? const Color(0xFF66BB6A)
|
||||
: const Color(0xFFFF9800))),
|
||||
])),
|
||||
],
|
||||
]),
|
||||
@@ -255,47 +307,78 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
|
||||
// 同步开关
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))]),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))
|
||||
]),
|
||||
child: Row(children: [
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(_syncEnabled ? Icons.sync : Icons.sync_disabled,
|
||||
color: _syncEnabled ? const Color(0xFF1A1A1A) : const Color(0xFFCCCCCC), size: 22)),
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(_syncEnabled ? Icons.sync : Icons.sync_disabled,
|
||||
color: _syncEnabled
|
||||
? colors.primary
|
||||
: colors.onSurface.withValues(alpha: 0.25),
|
||||
size: 22)),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const Text('服务端实时同步', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('服务端实时同步',
|
||||
style: TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
const SizedBox(height: 2),
|
||||
Text(_syncEnabled ? '使用服务端数据,多设备实时共享' : '关闭后下载数据到本地使用',
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))),
|
||||
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||
])),
|
||||
Switch(value: _syncEnabled, onChanged: _isActivated ? _toggleSync : null,
|
||||
activeColor: const Color(0xFF1A1A1A), activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3),
|
||||
inactiveThumbColor: Colors.white, inactiveTrackColor: const Color(0xFFE5E5E5)),
|
||||
Switch(
|
||||
value: _syncEnabled,
|
||||
onChanged: _isActivated ? _toggleSync : null,
|
||||
activeThumbColor: colors.primary,
|
||||
activeTrackColor: colors.primary.withValues(alpha: 0.3),
|
||||
inactiveThumbColor: colors.surface,
|
||||
inactiveTrackColor: colors.outline),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// 说明
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))]),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))
|
||||
]),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Container(width: 36, height: 36, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(8)),
|
||||
child: const Icon(Icons.info_outline, size: 18, color: Color(0xFF666666))),
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(8)),
|
||||
child: Icon(Icons.info_outline,
|
||||
size: 18, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
const SizedBox(width: 10),
|
||||
const Text('使用说明', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text('使用说明',
|
||||
style: TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
]),
|
||||
const SizedBox(height: 14),
|
||||
_infoItem('1. 在服务端管理后台生成激活码'),
|
||||
_infoItem(colors, '1. 在服务端管理后台生成激活码'),
|
||||
const SizedBox(height: 8),
|
||||
_infoItem('2. 输入服务器地址和激活码完成验证'),
|
||||
_infoItem(colors, '2. 输入服务器地址和激活码完成验证'),
|
||||
const SizedBox(height: 8),
|
||||
_infoItem('3. 验证通过后自动开启实时同步'),
|
||||
_infoItem(colors, '3. 验证通过后自动开启实时同步'),
|
||||
const SizedBox(height: 8),
|
||||
_infoItem('4. 开启时所有数据通过服务端接口操作'),
|
||||
_infoItem(colors, '4. 开启时所有数据通过服务端接口操作'),
|
||||
const SizedBox(height: 8),
|
||||
_infoItem('5. 关闭时从服务端下载数据到本地使用'),
|
||||
_infoItem(colors, '5. 关闭时从服务端下载数据到本地使用'),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
@@ -303,21 +386,33 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
|
||||
);
|
||||
}
|
||||
|
||||
InputDecoration _inputDeco(String hint) {
|
||||
InputDecoration _inputDeco(ColorScheme colors, String hint) {
|
||||
return InputDecoration(
|
||||
hintText: hint, hintStyle: const TextStyle(fontSize: 13, color: Color(0xFFCCCCCC)),
|
||||
filled: true, fillColor: const Color(0xFFF8F8F8),
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
filled: true,
|
||||
fillColor: colors.surfaceContainerHigh,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: colors.primary, width: 1)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoItem(String text) {
|
||||
Widget _infoItem(ColorScheme colors, String text) {
|
||||
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Container(width: 5, height: 5, margin: const EdgeInsets.only(top: 6), decoration: BoxDecoration(color: const Color(0xFFBBBBBB), shape: BoxShape.circle)),
|
||||
Container(
|
||||
width: 5,
|
||||
height: 5,
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.onSurface.withValues(alpha: 0.3), shape: BoxShape.circle)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(text, style: const TextStyle(fontSize: 13, color: Color(0xFF888888), height: 1.5))),
|
||||
Expanded(
|
||||
child: Text(text,
|
||||
style: TextStyle(
|
||||
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5), height: 1.5))),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,22 +66,37 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
final password = _passwordController.text;
|
||||
final path = _pathController.text.trim();
|
||||
|
||||
if (url.isEmpty) { ToastUtil.show(context, '请输入服务器地址'); return; }
|
||||
if (username.isEmpty) { ToastUtil.show(context, '请输入用户名'); return; }
|
||||
if (password.isEmpty) { ToastUtil.show(context, '请输入密码'); return; }
|
||||
if (url.isEmpty) {
|
||||
ToastUtil.show(context, '请输入服务器地址');
|
||||
return;
|
||||
}
|
||||
if (username.isEmpty) {
|
||||
ToastUtil.show(context, '请输入用户名');
|
||||
return;
|
||||
}
|
||||
if (password.isEmpty) {
|
||||
ToastUtil.show(context, '请输入密码');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
final result = await WebDAVService.instance.testConnection(
|
||||
url: url, username: username, password: password, path: path,
|
||||
url: url,
|
||||
username: username,
|
||||
password: password,
|
||||
path: path,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (result['success'] == true) {
|
||||
await WebDAVService.instance.saveConfig(
|
||||
url: url, username: username, password: password, path: path,
|
||||
url: url,
|
||||
username: username,
|
||||
password: password,
|
||||
path: path,
|
||||
);
|
||||
setState(() => _isConfigured = true);
|
||||
ToastUtil.show(context, result['message'] ?? '连接成功,配置已保存');
|
||||
@@ -104,7 +119,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
|
||||
if (result.success) {
|
||||
final details = '上传: ${result.uploadedFiles} 文件, ${result.uploadedImages} 图片\n'
|
||||
'下载: ${result.downloadedFiles} 文件, ${result.downloadedImages} 图片';
|
||||
'下载: ${result.downloadedFiles} 文件, ${result.downloadedImages} 图片';
|
||||
|
||||
if (result.needReload) {
|
||||
ToastUtil.show(context, '数据已更新,正在重新加载...');
|
||||
@@ -129,39 +144,48 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
void _showResultDialog(String title, String content) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 48, height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(Icons.check, color: colors.primary, size: 24),
|
||||
),
|
||||
child: const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 24),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(title, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
],
|
||||
),
|
||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(content, style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6), textAlign: TextAlign.center),
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
style: TextButton.styleFrom(minimumSize: const Size(120, 40), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
child: const Text('确定', style: TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))),
|
||||
const SizedBox(height: 16),
|
||||
Text(title,
|
||||
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(content,
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6),
|
||||
textAlign: TextAlign.center),
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: const Size(120, 40),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
child: Text('确定', style: TextStyle(fontSize: 14, color: colors.primary)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,25 +212,40 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
final selected = await showModalBottomSheet<int>(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
builder: (ctx) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Container(width: 36, height: 4, decoration: BoxDecoration(color: const Color(0xFFDDDDDD), borderRadius: BorderRadius.circular(2))),
|
||||
const SizedBox(height: 12),
|
||||
const Text('同步间隔', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
const SizedBox(height: 8),
|
||||
...intervals.map((i) => ListTile(
|
||||
title: Text('$i 分钟', style: TextStyle(fontSize: 15, fontWeight: _autoSyncInterval == i ? FontWeight.w600 : FontWeight.w400, color: const Color(0xFF1A1A1A))),
|
||||
trailing: _autoSyncInterval == i ? const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 20) : null,
|
||||
onTap: () => Navigator.pop(ctx, i),
|
||||
)),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
|
||||
const SizedBox(height: 12),
|
||||
Text('同步间隔',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
const SizedBox(height: 8),
|
||||
...intervals.map((i) => ListTile(
|
||||
title: Text('$i 分钟',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: _autoSyncInterval == i ? FontWeight.w600 : FontWeight.w400,
|
||||
color: colors.onSurface)),
|
||||
trailing: _autoSyncInterval == i
|
||||
? Icon(Icons.check, color: colors.primary, size: 20)
|
||||
: null,
|
||||
onTap: () => Navigator.pop(ctx, i),
|
||||
)),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (selected != null && selected != _autoSyncInterval) {
|
||||
@@ -226,41 +265,54 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
Future<void> _clearConfig() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40, height: 40,
|
||||
decoration: BoxDecoration(color: Colors.red.withOpacity(0.08), borderRadius: BorderRadius.circular(10)),
|
||||
child: const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.08), borderRadius: BorderRadius.circular(10)),
|
||||
child: const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('清除配置',
|
||||
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
],
|
||||
),
|
||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Text('确定要清除 WebDAV 配置吗?',
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6)),
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
child: Text('取消',
|
||||
style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6), fontSize: 14)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
child: const Text('清除',
|
||||
style: TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text('清除配置', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
],
|
||||
),
|
||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
content: const Padding(
|
||||
padding: EdgeInsets.only(top: 16),
|
||||
child: Text('确定要清除 WebDAV 配置吗?', style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6)),
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666), fontSize: 14)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
child: const Text('清除', style: TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
@@ -281,46 +333,54 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(title: const Text('WebDAV 备份')),
|
||||
body: _isLoading && !_isConfigured
|
||||
? const Center(child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A)))
|
||||
? Center(child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary))
|
||||
: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 已连接提示
|
||||
if (_isConfigured) _buildConnectedBanner(),
|
||||
if (_isConfigured) _buildConnectedBanner(colors),
|
||||
|
||||
// 服务器配置
|
||||
_buildSectionLabel('服务器配置'),
|
||||
_buildSectionLabel(colors, '服务器配置'),
|
||||
const SizedBox(height: 16),
|
||||
_buildInput(
|
||||
colors: colors,
|
||||
controller: _urlController,
|
||||
hint: '服务器地址,如 https://dav.example.com',
|
||||
icon: Icons.link,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInput(
|
||||
colors: colors,
|
||||
controller: _usernameController,
|
||||
hint: '用户名',
|
||||
icon: Icons.person_outline,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInput(
|
||||
colors: colors,
|
||||
controller: _passwordController,
|
||||
hint: '密码',
|
||||
icon: Icons.lock_outline,
|
||||
obscure: _obscurePassword,
|
||||
suffix: GestureDetector(
|
||||
onTap: () => setState(() => _obscurePassword = !_obscurePassword),
|
||||
child: Icon(_obscurePassword ? Icons.visibility_off : Icons.visibility, size: 20, color: const Color(0xFFBBBBBB)),
|
||||
child: Icon(
|
||||
_obscurePassword ? Icons.visibility_off : Icons.visibility,
|
||||
size: 20,
|
||||
color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInput(
|
||||
colors: colors,
|
||||
controller: _pathController,
|
||||
hint: '同步路径,如 /mooknote',
|
||||
icon: Icons.folder_outlined,
|
||||
@@ -328,14 +388,15 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 测试并保存
|
||||
_buildBtn('测试并保存', onTap: _isLoading ? null : _saveConfig),
|
||||
_buildBtn(colors, '测试并保存', onTap: _isLoading ? null : _saveConfig),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
if (_isConfigured) ...[
|
||||
// 自动同步
|
||||
_buildSectionLabel('自动同步'),
|
||||
_buildSectionLabel(colors, '自动同步'),
|
||||
const SizedBox(height: 12),
|
||||
_buildSwitchRow(
|
||||
colors: colors,
|
||||
icon: Icons.sync,
|
||||
label: '自动同步',
|
||||
sub: _isAutoSyncEnabled ? '每 $_autoSyncInterval 分钟自动同步' : '关闭',
|
||||
@@ -345,6 +406,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
if (_isAutoSyncEnabled) ...[
|
||||
const SizedBox(height: 4),
|
||||
_buildTappableRow(
|
||||
colors: colors,
|
||||
label: '同步间隔',
|
||||
value: '$_autoSyncInterval 分钟',
|
||||
onTap: _isLoading ? null : _showIntervalPicker,
|
||||
@@ -354,27 +416,31 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 手动同步
|
||||
_buildSectionLabel('手动同步'),
|
||||
_buildSectionLabel(colors, '手动同步'),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildDirectionChip('上传到云端', SyncDirection.upload, Icons.upload)),
|
||||
Expanded(
|
||||
child: _buildDirectionChip(
|
||||
colors, '上传到云端', SyncDirection.upload, Icons.upload)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildDirectionChip('下载到本地', SyncDirection.download, Icons.download)),
|
||||
Expanded(
|
||||
child: _buildDirectionChip(
|
||||
colors, '下载到本地', SyncDirection.download, Icons.download)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildBtn('立即同步', onTap: _isLoading ? null : _syncData, loading: _isLoading),
|
||||
_buildBtn(colors, '立即同步', onTap: _isLoading ? null : _syncData, loading: _isLoading),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 清除配置
|
||||
_buildTextBtn('清除配置', onTap: _isLoading ? null : _clearConfig),
|
||||
_buildTextBtn(colors, '清除配置', onTap: _isLoading ? null : _clearConfig),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
|
||||
const SizedBox(height: 32),
|
||||
_buildTips(),
|
||||
_buildTips(colors),
|
||||
const SizedBox(height: 60),
|
||||
],
|
||||
),
|
||||
@@ -383,40 +449,48 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
|
||||
// ── widgets ─────────────────────────────────────────
|
||||
|
||||
Widget _buildConnectedBanner() {
|
||||
Widget _buildConnectedBanner(ColorScheme colors) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 24),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 6, height: 6,
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: const BoxDecoration(color: Color(0xFF4CAF50), shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_urlController.text,
|
||||
style: const TextStyle(fontSize: 13, color: Color(0xFF666666)),
|
||||
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6)),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Text('已连接', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
Text('已连接',
|
||||
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionLabel(String text) {
|
||||
return Text(text, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF999999), letterSpacing: 0.5));
|
||||
Widget _buildSectionLabel(ColorScheme colors, String text) {
|
||||
return Text(text,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
letterSpacing: 0.5));
|
||||
}
|
||||
|
||||
Widget _buildInput({
|
||||
required ColorScheme colors,
|
||||
required TextEditingController controller,
|
||||
required String hint,
|
||||
required IconData icon,
|
||||
@@ -426,35 +500,37 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
obscureText: obscure,
|
||||
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
|
||||
style: TextStyle(fontSize: 15, color: colors.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(fontSize: 15, color: Color(0xFFBBBBBB)),
|
||||
hintStyle: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
prefixIcon: Padding(
|
||||
padding: const EdgeInsets.only(left: 4, right: 8),
|
||||
child: Icon(icon, size: 20, color: const Color(0xFFBBBBBB)),
|
||||
child: Icon(icon, size: 20, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
),
|
||||
prefixIconConstraints: const BoxConstraints(minWidth: 44),
|
||||
suffixIcon: suffix != null ? Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: suffix,
|
||||
) : null,
|
||||
suffixIcon: suffix != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: suffix,
|
||||
)
|
||||
: null,
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFF8F8F8),
|
||||
fillColor: colors.surfaceContainerHigh,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1),
|
||||
borderSide: BorderSide(color: colors.primary, width: 1),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBtn(String text, {VoidCallback? onTap, bool loading = false}) {
|
||||
Widget _buildBtn(ColorScheme colors, String text, {VoidCallback? onTap, bool loading = false}) {
|
||||
final disabled = onTap == null;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
@@ -462,30 +538,44 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: disabled ? const Color(0xFFDDDDDD) : const Color(0xFF1A1A1A),
|
||||
color: disabled ? colors.onSurface.withValues(alpha: 0.15) : colors.primary,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Center(
|
||||
child: loading
|
||||
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white)))
|
||||
: Text(text, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white)),
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, valueColor: AlwaysStoppedAnimation(colors.onPrimary)))
|
||||
: Text(text,
|
||||
style: TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.w600, color: colors.onPrimary)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextBtn(String text, {VoidCallback? onTap}) {
|
||||
Widget _buildTextBtn(ColorScheme colors, String text, {VoidCallback? onTap}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(text, style: TextStyle(fontSize: 14, color: onTap == null ? const Color(0xFFCCCCCC) : const Color(0xFF999999)))),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: onTap == null
|
||||
? colors.onSurface.withValues(alpha: 0.25)
|
||||
: colors.onSurface.withValues(alpha: 0.4))),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSwitchRow({
|
||||
required ColorScheme colors,
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String sub,
|
||||
@@ -495,26 +585,30 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F8F8),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: value ? const Color(0xFF1A1A1A) : const Color(0xFFBBBBBB)),
|
||||
Icon(icon,
|
||||
size: 20,
|
||||
color: value ? colors.primary : colors.onSurface.withValues(alpha: 0.3)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value ? sub : label,
|
||||
style: TextStyle(fontSize: 14, color: value ? const Color(0xFF666666) : const Color(0xFF1A1A1A)),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: value ? colors.onSurface.withValues(alpha: 0.6) : colors.onSurface),
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: const Color(0xFF1A1A1A),
|
||||
activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3),
|
||||
inactiveThumbColor: Colors.white,
|
||||
inactiveTrackColor: const Color(0xFFDDDDDD),
|
||||
activeThumbColor: colors.primary,
|
||||
activeTrackColor: colors.primary.withValues(alpha: 0.3),
|
||||
inactiveThumbColor: colors.surface,
|
||||
inactiveTrackColor: colors.onSurface.withValues(alpha: 0.15),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -522,6 +616,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
}
|
||||
|
||||
Widget _buildTappableRow({
|
||||
required ColorScheme colors,
|
||||
required String label,
|
||||
required String value,
|
||||
VoidCallback? onTap,
|
||||
@@ -531,77 +626,99 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F8F8),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 32),
|
||||
Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF999999))),
|
||||
Text(label,
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const Spacer(),
|
||||
Text(value, style: const TextStyle(fontSize: 14, color: Color(0xFF666666))),
|
||||
Text(value,
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.chevron_right, size: 16, color: Color(0xFFCCCCCC)),
|
||||
Icon(Icons.chevron_right,
|
||||
size: 16, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDirectionChip(String label, SyncDirection dir, IconData icon) {
|
||||
Widget _buildDirectionChip(ColorScheme colors, String label, SyncDirection dir, IconData icon) {
|
||||
final selected = _syncDirection == dir;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _syncDirection = dir),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFF8F8F8),
|
||||
color: selected ? colors.primary : colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: selected ? Colors.white : const Color(0xFF999999)),
|
||||
Icon(icon,
|
||||
size: 16,
|
||||
color: selected
|
||||
? colors.onPrimary
|
||||
: colors.onSurface.withValues(alpha: 0.4)),
|
||||
const SizedBox(width: 6),
|
||||
Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: selected ? Colors.white : const Color(0xFF666666))),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: selected
|
||||
? colors.onPrimary
|
||||
: colors.onSurface.withValues(alpha: 0.6))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTips() {
|
||||
Widget _buildTips(ColorScheme colors) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F8F8),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('支持的服务', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF999999), letterSpacing: 0.5)),
|
||||
Text('支持的服务',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
letterSpacing: 0.5)),
|
||||
const SizedBox(height: 10),
|
||||
_tip('坚果云、Nextcloud、AList 等 WebDAV 服务'),
|
||||
_tip('服务器地址需包含 https://'),
|
||||
_tip('首次同步可能需要较长时间'),
|
||||
_tip(colors, '坚果云、Nextcloud、AList 等 WebDAV 服务'),
|
||||
_tip(colors, '服务器地址需包含 https://'),
|
||||
_tip(colors, '首次同步可能需要较长时间'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tip(String text) {
|
||||
Widget _tip(ColorScheme colors, String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
child: Icon(Icons.circle, size: 4, color: Color(0xFFCCCCCC)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Icon(Icons.circle,
|
||||
size: 4, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(text, style: const TextStyle(fontSize: 13, color: Color(0xFF888888), height: 1.5))),
|
||||
Expanded(
|
||||
child: Text(text,
|
||||
style: TextStyle(
|
||||
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5), height: 1.5))),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -48,7 +48,6 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
|
||||
String get _currentType => _tabTypes[_currentIndex];
|
||||
|
||||
/// 计算标签使用次数
|
||||
Map<String, int> _getTagUsageCounts(String type) {
|
||||
final provider = context.read<AppProvider>();
|
||||
final counts = <String, int>{};
|
||||
@@ -81,16 +80,17 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F8F8),
|
||||
backgroundColor: colors.surfaceContainerHigh,
|
||||
appBar: AppBar(
|
||||
title: const Text('标签管理'),
|
||||
actions: [
|
||||
_isSyncing
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SizedBox(width: 20, height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A))),
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)),
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.sync, size: 20),
|
||||
@@ -112,7 +112,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 12, offset: const Offset(0, 4)),
|
||||
@@ -121,10 +121,10 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.add, size: 18, color: Colors.white),
|
||||
Icon(Icons.add, size: 18, color: colors.onPrimary),
|
||||
const SizedBox(width: 6),
|
||||
Text('添加${_typeLabels[_currentIndex]}',
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -132,14 +132,13 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Tab 选择器 ──────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildTabSelector() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEBEBEB),
|
||||
color: colors.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
@@ -158,7 +157,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
padding: const EdgeInsets.all(3),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, 2)),
|
||||
@@ -183,7 +182,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
alignment: Alignment.center,
|
||||
child: Text(_typeLabels[i],
|
||||
style: TextStyle(fontSize: 13, fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: selected ? const Color(0xFF1A1A1A) : const Color(0xFF999999))),
|
||||
color: selected ? colors.onSurface : colors.onSurface.withValues(alpha: 0.4))),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -197,9 +196,8 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 标签列表 ────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildTagList(String type) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final tags = _tagCache[type] ?? [];
|
||||
final usageCounts = _getTagUsageCounts(type);
|
||||
|
||||
@@ -207,7 +205,6 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
return _buildEmptyState(type);
|
||||
}
|
||||
|
||||
// 按使用次数降序排序
|
||||
final sorted = List<Map<String, dynamic>>.from(tags)
|
||||
..sort((a, b) {
|
||||
final ca = usageCounts[a['name']] ?? 0;
|
||||
@@ -220,18 +217,16 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 统计行
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text('共 ${tags.length} 个标签', style: const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))),
|
||||
Text('共 ${tags.length} 个标签', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||
const Spacer(),
|
||||
Text('已使用 ${usageCounts.length} 个', style: const TextStyle(fontSize: 12, color: Color(0xFFD0D0D0))),
|
||||
Text('已使用 ${usageCounts.length} 个', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.2))),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 标签列表
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 80),
|
||||
@@ -248,50 +243,50 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
}
|
||||
|
||||
Widget _buildTagChip(Map<String, dynamic> tag, Map<String, int> usageCounts) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final name = tag['name'] as String;
|
||||
final count = usageCounts[name] ?? 0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 14, right: 6, top: 8, bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: const Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 标签名区域 — 点击改名
|
||||
GestureDetector(
|
||||
onTap: () => _showRenameDialog(tag),
|
||||
child: Text(name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
||||
),
|
||||
if (count > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(color: const Color(0xFFF0F0F0), borderRadius: BorderRadius.circular(8)),
|
||||
child: Text('$count', style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: Color(0xFF999999))),
|
||||
return GestureDetector(
|
||||
onLongPress: () => _showRenameDialog(tag),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 14, right: 6, top: 8, bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: const Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(name, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
if (count > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(color: colors.outlineVariant, borderRadius: BorderRadius.circular(8)),
|
||||
child: Text('$count', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 4),
|
||||
GestureDetector(
|
||||
onTap: () => _showDeleteDialog(tag),
|
||||
child: Container(
|
||||
width: 22, height: 22,
|
||||
decoration: BoxDecoration(color: colors.outlineVariant, borderRadius: BorderRadius.circular(11)),
|
||||
child: Icon(Icons.close, size: 12, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 4),
|
||||
// X 按钮 — 点击删除
|
||||
GestureDetector(
|
||||
onTap: () => _showDeleteDialog(tag),
|
||||
child: Container(
|
||||
width: 22, height: 22,
|
||||
decoration: BoxDecoration(color: const Color(0xFFF0F0F0), borderRadius: BorderRadius.circular(11)),
|
||||
child: const Icon(Icons.close, size: 12, color: Color(0xFF999999)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(String type) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final idx = _tabTypes.indexOf(type);
|
||||
final icon = _typeIcons[idx];
|
||||
final label = _typeLabels[idx];
|
||||
@@ -303,57 +298,56 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
children: [
|
||||
Container(
|
||||
width: 64, height: 64,
|
||||
decoration: BoxDecoration(color: const Color(0xFFECECEC), borderRadius: BorderRadius.circular(18)),
|
||||
child: Icon(icon, size: 28, color: const Color(0xFFCCCCCC)),
|
||||
decoration: BoxDecoration(color: colors.outlineVariant, borderRadius: BorderRadius.circular(18)),
|
||||
child: Icon(icon, size: 28, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('暂无$label',
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFFBBBBBB), fontWeight: FontWeight.w500)),
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.3), fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 6),
|
||||
Text(hints[idx], style: const TextStyle(fontSize: 12, color: Color(0xFFD5D5D5))),
|
||||
Text(hints[idx], style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.15))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 添加标签 ────────────────────────────────────────────────────────
|
||||
|
||||
void _showAddDialog() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final controller = TextEditingController();
|
||||
final type = _currentType;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
title: Text('添加${_typeLabels[_currentIndex]}',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
content: TextField(
|
||||
controller: controller, autofocus: true,
|
||||
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
|
||||
style: TextStyle(fontSize: 15, color: colors.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入标签名称',
|
||||
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)),
|
||||
filled: true, fillColor: const Color(0xFFFAFAFA),
|
||||
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||
filled: true, fillColor: colors.surfaceContainerHigh,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1)),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: colors.primary, width: 1)),
|
||||
),
|
||||
onSubmitted: (value) => _doAddTag(ctx, controller.text.trim(), type),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
|
||||
Container(
|
||||
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(20)),
|
||||
decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _doAddTag(ctx, controller.text.trim(), type),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
child: Text('添加', style: TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
child: Text('添加', style: TextStyle(fontSize: 14, color: colors.onPrimary, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -378,9 +372,8 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
await _loadTags(type);
|
||||
}
|
||||
|
||||
// ─── 重命名 ──────────────────────────────────────────────────────────
|
||||
|
||||
void _showRenameDialog(Map<String, dynamic> tag) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final controller = TextEditingController(text: tag['name'] as String);
|
||||
final tagId = tag['id'] as String;
|
||||
final type = tag['type'] as String;
|
||||
@@ -389,34 +382,34 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
title: const Text('重命名标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
title: Text('重命名标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
content: TextField(
|
||||
controller: controller, autofocus: true,
|
||||
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
|
||||
style: TextStyle(fontSize: 15, color: colors.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入新名称',
|
||||
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)),
|
||||
filled: true, fillColor: const Color(0xFFFAFAFA),
|
||||
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||
filled: true, fillColor: colors.surfaceContainerHigh,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1)),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: colors.primary, width: 1)),
|
||||
),
|
||||
onSubmitted: (value) => _doRenameTag(ctx, tagId, value.trim(), type, oldName),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
|
||||
Container(
|
||||
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(20)),
|
||||
decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _doRenameTag(ctx, tagId, controller.text.trim(), type, oldName),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
child: Text('确定', style: TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
child: Text('确定', style: TextStyle(fontSize: 14, color: colors.onPrimary, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -439,9 +432,8 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
if (success) await _loadTags(type);
|
||||
}
|
||||
|
||||
// ─── 删除(长按触发)──────────────────────────────────────────────────
|
||||
|
||||
void _showDeleteDialog(Map<String, dynamic> tag) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final tagId = tag['id'] as String;
|
||||
final type = tag['type'] as String;
|
||||
final name = tag['name'] as String;
|
||||
@@ -457,17 +449,17 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setDialogState) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(12)),
|
||||
child: Text(name, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))),
|
||||
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(12)),
|
||||
child: Text(name, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text('删除标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text('删除标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
],
|
||||
),
|
||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
@@ -481,7 +473,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
const Text('删除后对已有条目的影响:', style: TextStyle(fontSize: 13, color: Color(0xFF999999))),
|
||||
Text('删除后对已有条目的影响:', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const SizedBox(height: 12),
|
||||
_buildDeleteOption(value: 'remove', groupValue: selectedAction, onChanged: (v) => setDialogState(() => selectedAction = v), title: '从所有条目中移除该标签', subtitle: '标签将从影视/书籍/笔记中清除'),
|
||||
const SizedBox(height: 4),
|
||||
@@ -504,19 +496,19 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5),
|
||||
color: isSelected ? colors.primary : colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: isSelected ? colors.primary : colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Text(t, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: isSelected ? Colors.white : const Color(0xFF555555))),
|
||||
child: Text(t, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.7))),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(12)),
|
||||
child: const Text('无其他标签可替换', style: TextStyle(fontSize: 13, color: Color(0xFFAAAAAA))),
|
||||
decoration: BoxDecoration(color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(12)),
|
||||
child: Text('无其他标签可替换', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -526,7 +518,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
|
||||
Container(
|
||||
decoration: BoxDecoration(color: const Color(0xFFE53935), borderRadius: BorderRadius.circular(20)),
|
||||
child: Material(
|
||||
@@ -573,15 +565,16 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
required String title,
|
||||
String? subtitle,
|
||||
}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final selected = value == groupValue;
|
||||
return GestureDetector(
|
||||
onTap: () => onChanged(value),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? const Color(0xFFFAFAFA) : Colors.white,
|
||||
color: selected ? colors.surfaceContainerHigh : colors.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFEEEEEE), width: selected ? 1 : 0.5),
|
||||
border: Border.all(color: selected ? colors.primary : colors.outlineVariant, width: selected ? 1 : 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -589,14 +582,14 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
||||
width: 18, height: 18,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFCCCCCC), width: selected ? 5 : 1.5),
|
||||
border: Border.all(color: selected ? colors.primary : colors.onSurface.withValues(alpha: 0.25), width: selected ? 5 : 1.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(title, style: TextStyle(fontSize: 14, fontWeight: selected ? FontWeight.w500 : FontWeight.normal, color: selected ? const Color(0xFF1A1A1A) : const Color(0xFF666666))),
|
||||
if (subtitle != null) Padding(padding: const EdgeInsets.only(top: 2), child: Text(subtitle, style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)))),
|
||||
Text(title, style: TextStyle(fontSize: 14, fontWeight: selected ? FontWeight.w500 : FontWeight.normal, color: selected ? colors.onSurface : colors.onSurface.withValues(alpha: 0.6))),
|
||||
if (subtitle != null) Padding(padding: const EdgeInsets.only(top: 2), child: Text(subtitle, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35)))),
|
||||
]),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -32,13 +32,16 @@ class AppProvider extends ChangeNotifier {
|
||||
|
||||
// 当前主界面选中的标签 (0: 观影,1: 阅读,2: 笔记)
|
||||
int _mainTabIndex = 0;
|
||||
|
||||
|
||||
// 当前底部导航选中的索引 (0: 主页,1: 新增,2: 我的)
|
||||
int _bottomNavIndex = 0;
|
||||
|
||||
// 底部导航栏是否可见
|
||||
bool _bottomNavVisible = true;
|
||||
|
||||
// 主题模式
|
||||
ThemeMode _themeMode = ThemeMode.system;
|
||||
|
||||
/// 是否使用远程服务端(同步开关 + 已激活)
|
||||
bool get _useRemote {
|
||||
final prefs = UserPrefs();
|
||||
@@ -134,6 +137,7 @@ class AppProvider extends ChangeNotifier {
|
||||
int get bookStatusIndex => _bookStatusIndex;
|
||||
bool get drawerOpen => _drawerOpen;
|
||||
bool get bottomNavVisible => _bottomNavVisible;
|
||||
ThemeMode get themeMode => _themeMode;
|
||||
List<Movie> get movies => _movies;
|
||||
List<Book> get books => _books;
|
||||
List<Note> get notes => _notes;
|
||||
@@ -167,6 +171,26 @@ class AppProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void setThemeMode(ThemeMode mode) {
|
||||
if (_themeMode != mode) {
|
||||
_themeMode = mode;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void loadThemeMode() {
|
||||
final prefs = UserPrefs();
|
||||
switch (prefs.themeMode) {
|
||||
case 1:
|
||||
_themeMode = ThemeMode.light;
|
||||
case 2:
|
||||
_themeMode = ThemeMode.dark;
|
||||
default:
|
||||
_themeMode = ThemeMode.system;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setMovieStatusIndex(int index) {
|
||||
_movieStatusIndex = index;
|
||||
notifyListeners();
|
||||
|
||||
@@ -51,6 +51,10 @@ class AppTheme {
|
||||
onSurface: _black,
|
||||
error: error,
|
||||
onError: _white,
|
||||
surfaceContainerHighest: _offWhite,
|
||||
surfaceContainerHigh: Color(0xFFFAFAFA),
|
||||
outline: _lighterGray,
|
||||
outlineVariant: Color(0xFFF0F0F0),
|
||||
),
|
||||
|
||||
// AppBar - 极简无边框
|
||||
@@ -262,6 +266,10 @@ class AppTheme {
|
||||
onSurface: _white,
|
||||
error: Color(0xFFEF4444),
|
||||
onError: _black,
|
||||
surfaceContainerHighest: _darkGray,
|
||||
surfaceContainerHigh: Color(0xFF2A2A2A),
|
||||
outline: _gray,
|
||||
outlineVariant: _darkGray,
|
||||
),
|
||||
|
||||
appBarTheme: const AppBarTheme(
|
||||
@@ -286,6 +294,12 @@ class AppTheme {
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
margin: EdgeInsets.zero,
|
||||
),
|
||||
|
||||
listTileTheme: const ListTileThemeData(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
minLeadingWidth: 0,
|
||||
dense: true,
|
||||
),
|
||||
|
||||
dividerTheme: DividerThemeData(
|
||||
color: _darkGray,
|
||||
@@ -304,6 +318,9 @@ class AppTheme {
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: _white, width: 1),
|
||||
),
|
||||
errorBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Color(0xFFEF4444), width: 0.5),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 12),
|
||||
hintStyle: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
@@ -311,6 +328,12 @@ class AppTheme {
|
||||
fontWeight: _regular,
|
||||
color: _gray,
|
||||
),
|
||||
labelStyle: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 13,
|
||||
fontWeight: _medium,
|
||||
color: _lightGray,
|
||||
),
|
||||
),
|
||||
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
@@ -327,6 +350,11 @@ class AppTheme {
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: _white,
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
textStyle: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 14,
|
||||
fontWeight: _medium,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -335,6 +363,84 @@ class AppTheme {
|
||||
selectedItemColor: _white,
|
||||
unselectedItemColor: _gray,
|
||||
elevation: 0,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
selectedLabelStyle: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 11,
|
||||
fontWeight: _medium,
|
||||
),
|
||||
unselectedLabelStyle: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 11,
|
||||
fontWeight: _regular,
|
||||
),
|
||||
),
|
||||
|
||||
textTheme: const TextTheme(
|
||||
headlineLarge: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 32,
|
||||
fontWeight: _semibold,
|
||||
color: _white,
|
||||
letterSpacing: -0.5,
|
||||
height: 1.2,
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 24,
|
||||
fontWeight: _semibold,
|
||||
color: _white,
|
||||
letterSpacing: -0.3,
|
||||
height: 1.3,
|
||||
),
|
||||
headlineSmall: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 20,
|
||||
fontWeight: _semibold,
|
||||
color: _white,
|
||||
letterSpacing: -0.2,
|
||||
height: 1.4,
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 16,
|
||||
fontWeight: _regular,
|
||||
color: _offWhite,
|
||||
height: 1.6,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 15,
|
||||
fontWeight: _regular,
|
||||
color: _offWhite,
|
||||
height: 1.5,
|
||||
),
|
||||
bodySmall: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 13,
|
||||
fontWeight: _regular,
|
||||
color: _lightGray,
|
||||
height: 1.5,
|
||||
),
|
||||
labelLarge: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 14,
|
||||
fontWeight: _medium,
|
||||
color: _white,
|
||||
),
|
||||
labelMedium: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 12,
|
||||
fontWeight: _medium,
|
||||
color: _lightGray,
|
||||
),
|
||||
labelSmall: TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 11,
|
||||
fontWeight: _medium,
|
||||
color: _gray,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ class UsageStatsService with WidgetsBindingObserver {
|
||||
final UserPrefs _prefs = UserPrefs();
|
||||
|
||||
/// 统计服务器地址,发布前替换为实际地址,置空则禁用
|
||||
static String serverUrl = 'http://api.mooknote.iletter.top/';
|
||||
|
||||
// static String serverUrl = 'http://api.mooknote.iletter.top/';
|
||||
static String serverUrl = 'http://192.168.31.48:27050/';
|
||||
Timer? _heartbeatTimer;
|
||||
bool _started = false;
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ class UserPrefs {
|
||||
/// 初始化
|
||||
static Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
// 迁移旧版 isDarkMode 布尔值到新版 themeMode 三态值
|
||||
if (_prefs!.containsKey('isDarkMode') && !_prefs!.containsKey('themeMode')) {
|
||||
final oldValue = _prefs!.getBool('isDarkMode') ?? false;
|
||||
await _prefs!.setInt('themeMode', oldValue ? 2 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取实例
|
||||
@@ -38,9 +43,9 @@ class UserPrefs {
|
||||
|
||||
// ========== 应用设置 ==========
|
||||
|
||||
/// 是否暗黑模式
|
||||
bool get isDarkMode => prefs.getBool('isDarkMode') ?? false;
|
||||
Future<bool> setDarkMode(bool value) => prefs.setBool('isDarkMode', value);
|
||||
/// 主题模式: 0=跟随系统, 1=浅色, 2=深色
|
||||
int get themeMode => prefs.getInt('themeMode') ?? 0;
|
||||
Future<bool> setThemeMode(int value) => prefs.setInt('themeMode', value);
|
||||
|
||||
/// 是否首次启动
|
||||
bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true;
|
||||
|
||||
@@ -55,6 +55,7 @@ class _AnimatedStarRatingState extends State<AnimatedStarRating>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final starValue = widget.rating / 2;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -84,7 +85,7 @@ class _AnimatedStarRatingState extends State<AnimatedStarRating>
|
||||
style: TextStyle(
|
||||
fontSize: widget.starSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -15,10 +15,11 @@ class AppRefreshIndicator extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return RefreshIndicator(
|
||||
onRefresh: onRefresh,
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: Colors.white,
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.surface,
|
||||
strokeWidth: 2.5,
|
||||
displacement: 60,
|
||||
semanticsLabel: semanticsLabel,
|
||||
|
||||
@@ -15,6 +15,7 @@ class BookListItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, '/book-detail', arguments: book);
|
||||
@@ -23,28 +24,21 @@ class BookListItem extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 封面
|
||||
Expanded(
|
||||
child: _buildCover(),
|
||||
child: _buildCover(colors),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 书名
|
||||
Text(
|
||||
book.title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// 评分
|
||||
if (book.rating != null)
|
||||
AnimatedStarRating(rating: book.rating!, starSize: 12, showNumber: true)
|
||||
else
|
||||
@@ -53,45 +47,45 @@ class BookListItem extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建封面
|
||||
Widget _buildCover() {
|
||||
|
||||
Widget _buildCover(ColorScheme colors) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5),
|
||||
border: Border.all(color: colors.outline, width: 0.5),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: FadeInLocalImage(
|
||||
path: book.coverPath,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: const Center(child: Icon(Icons.menu_book_outlined, size: 32, color: Color(0xFFCCCCCC))),
|
||||
errorWidget: const Center(child: Icon(Icons.menu_book_outlined, size: 32, color: Color(0xFFCCCCCC))),
|
||||
placeholder: Center(child: Icon(Icons.menu_book_outlined, size: 32, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||
errorWidget: Center(child: Icon(Icons.menu_book_outlined, size: 32, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示删除确认对话框
|
||||
void _showDeleteDialog(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text(
|
||||
title: Text(
|
||||
'确认删除',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
content: Text(
|
||||
'确定要删除《${book.title}》吗?删除后可在回收站恢复。',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -99,7 +93,7 @@ class BookListItem extends StatelessWidget {
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF666666),
|
||||
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
child: const Text('取消'),
|
||||
@@ -111,8 +105,8 @@ class BookListItem extends StatelessWidget {
|
||||
ToastUtil.show(context, '已删除');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colors.error,
|
||||
foregroundColor: colors.onError,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
|
||||
@@ -8,20 +8,21 @@ class BookStatusBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
|
||||
bottom: BorderSide(color: colors.outline, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
@@ -42,7 +43,7 @@ class BookStatusBar extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(3),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -58,18 +59,21 @@ class BookStatusBar extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
_buildTab(
|
||||
colors: colors,
|
||||
label: '已读',
|
||||
icon: Icons.check_circle_outline,
|
||||
isSelected: provider.bookStatusIndex == 0,
|
||||
onTap: () => provider.setBookStatusIndex(0),
|
||||
),
|
||||
_buildTab(
|
||||
colors: colors,
|
||||
label: '在读',
|
||||
icon: Icons.menu_book_outlined,
|
||||
isSelected: provider.bookStatusIndex == 1,
|
||||
onTap: () => provider.setBookStatusIndex(1),
|
||||
),
|
||||
_buildTab(
|
||||
colors: colors,
|
||||
label: '想读',
|
||||
icon: Icons.bookmark_outlined,
|
||||
isSelected: provider.bookStatusIndex == 2,
|
||||
@@ -89,6 +93,7 @@ class BookStatusBar extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildTab({
|
||||
required ColorScheme colors,
|
||||
required String label,
|
||||
required IconData icon,
|
||||
required bool isSelected,
|
||||
@@ -108,7 +113,7 @@ class BookStatusBar extends StatelessWidget {
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: isSelected ? Colors.white : const Color(0xFF666666),
|
||||
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
@@ -116,7 +121,7 @@ class BookStatusBar extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: isSelected ? Colors.white : const Color(0xFF666666),
|
||||
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -8,34 +8,33 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 获取底部安全区域高度
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return Container(
|
||||
// 高度:导航栏本身高度 + 底部安全距离 + 上下边距
|
||||
height: 64 + bottomPadding + 16,
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Dock栏主体 - 悬浮效果
|
||||
Container(
|
||||
height: 56,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 40),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
color: Colors.black.withOpacity(isDark ? 0.3 : 0.08),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
color: Colors.black.withOpacity(isDark ? 0.15 : 0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
spreadRadius: -2,
|
||||
@@ -45,19 +44,16 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// 主页按钮
|
||||
_buildNavItem(
|
||||
colors: colors,
|
||||
icon: Icons.home_outlined,
|
||||
activeIcon: Icons.home,
|
||||
isActive: provider.bottomNavIndex == 0,
|
||||
onTap: () => provider.setBottomNavIndex(0),
|
||||
),
|
||||
|
||||
// 中间新增按钮
|
||||
_buildAddButton(context, provider),
|
||||
|
||||
// 我的按钮
|
||||
_buildNavItem(
|
||||
colors: colors,
|
||||
icon: Icons.person_outline,
|
||||
activeIcon: Icons.person,
|
||||
isActive: provider.bottomNavIndex == 2,
|
||||
@@ -66,7 +62,6 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
// 底部安全距离占位
|
||||
SizedBox(height: bottomPadding + 8),
|
||||
],
|
||||
),
|
||||
@@ -74,9 +69,9 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建导航项
|
||||
|
||||
Widget _buildNavItem({
|
||||
required ColorScheme colors,
|
||||
required IconData icon,
|
||||
required IconData activeIcon,
|
||||
required bool isActive,
|
||||
@@ -92,16 +87,16 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
child: Center(
|
||||
child: Icon(
|
||||
isActive ? activeIcon : icon,
|
||||
color: isActive ? const Color(0xFF1A1A1A) : const Color(0xFF999999),
|
||||
color: isActive ? colors.primary : colors.onSurface.withValues(alpha: 0.4),
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建中间新增按钮
|
||||
|
||||
Widget _buildAddButton(BuildContext context, AppProvider provider) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: () => _showAddDialog(context, provider),
|
||||
onLongPress: () => _showQuickAddDialog(context, provider),
|
||||
@@ -109,25 +104,23 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
color: colors.onPrimary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 长按快速添加 - 根据当前界面直接跳转到对应添加界面
|
||||
void _showQuickAddDialog(BuildContext context, AppProvider provider) {
|
||||
// 根据当前主标签页决定跳转到哪个添加界面
|
||||
final currentTab = provider.mainTabIndex;
|
||||
|
||||
|
||||
switch (currentTab) {
|
||||
case 0: // 观影标签页
|
||||
case 0:
|
||||
final statusMap = {
|
||||
0: 'watched',
|
||||
1: 'watching',
|
||||
@@ -140,7 +133,7 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
arguments: {'initialStatus': currentStatus},
|
||||
);
|
||||
break;
|
||||
case 1: // 阅读标签页
|
||||
case 1:
|
||||
final statusMap = {
|
||||
0: 'read',
|
||||
1: 'reading',
|
||||
@@ -153,7 +146,7 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
arguments: {'initialStatus': currentStatus},
|
||||
);
|
||||
break;
|
||||
case 2: // 笔记标签页
|
||||
case 2:
|
||||
Navigator.pushNamed(context, '/note-form');
|
||||
break;
|
||||
default:
|
||||
@@ -161,34 +154,33 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示新增对话框
|
||||
void _showAddDialog(BuildContext context, AppProvider provider) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
final bottomColors = Theme.of(context).colorScheme;
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 顶部指示条
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE0E0E0),
|
||||
color: bottomColors.onSurface.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 标题
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
@@ -196,15 +188,15 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: bottomColors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 选项列表
|
||||
_buildAddOption(
|
||||
colors: bottomColors,
|
||||
icon: Icons.movie_outlined,
|
||||
title: '添加观影',
|
||||
subtitle: '记录你看过的电影',
|
||||
@@ -224,6 +216,7 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
_buildAddOption(
|
||||
colors: bottomColors,
|
||||
icon: Icons.menu_book_outlined,
|
||||
title: '添加阅读',
|
||||
subtitle: '记录你读过的书',
|
||||
@@ -243,6 +236,7 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
_buildAddOption(
|
||||
colors: bottomColors,
|
||||
icon: Icons.note_outlined,
|
||||
title: '添加笔记',
|
||||
subtitle: '记录你的想法和笔记',
|
||||
@@ -259,8 +253,8 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建新增选项
|
||||
Widget _buildAddOption({
|
||||
required ColorScheme colors,
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
@@ -276,13 +270,13 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 22,
|
||||
color: const Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@@ -292,26 +286,26 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Color(0xFFCCCCCC),
|
||||
color: colors.onSurface.withValues(alpha: 0.25),
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -33,36 +33,25 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Drawer(
|
||||
backgroundColor: const Color(0xFFF8F8F8),
|
||||
backgroundColor: colors.surfaceContainerHigh,
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 头像 + 统计(独立卡片区域)
|
||||
_buildProfileCard(context),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 热力图
|
||||
_buildCalendarSection(context),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 最近添加
|
||||
_buildRecentSection(context),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 功能入口
|
||||
_buildToolsCard(context),
|
||||
|
||||
// 底部版本号
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 32),
|
||||
child: Center(
|
||||
child: Text('v$_version', style: const TextStyle(fontSize: 11, color: Color(0xFFD0D0D0))),
|
||||
child: Text('v$_version', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.2))),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -77,6 +66,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
Widget _buildProfileCard(BuildContext context) {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final userPrefs = UserPrefs();
|
||||
final nickname = userPrefs.nickname;
|
||||
final motto = userPrefs.motto;
|
||||
@@ -89,77 +79,69 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 头像 + 名称/座右铭 + 统计
|
||||
Row(
|
||||
children: [
|
||||
// 头像
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0xFFF5F5F5),
|
||||
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: avatarPath != null && avatarPath.isNotEmpty
|
||||
? Image.file(File(avatarPath), fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
const Icon(Icons.person_outline, size: 26, color: Color(0xFFBBBBBB)))
|
||||
: const Icon(Icons.person_outline, size: 26, color: Color(0xFFBBBBBB)),
|
||||
Icon(Icons.person_outline, size: 26, color: colors.onSurface.withValues(alpha: 0.3)))
|
||||
: Icon(Icons.person_outline, size: 26, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
// 名称 + 座右铭
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(nickname, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text(nickname, style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
const SizedBox(height: 2),
|
||||
Text(motto, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
|
||||
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const Divider(height: 1, color: Color(0xFFF0F0F0)),
|
||||
Divider(height: 1, color: colors.outlineVariant),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// 统计数字
|
||||
_buildProfileStatRow(Icons.movie_outlined, movieCount, '观影'),
|
||||
const SizedBox(height: 12),
|
||||
_buildProfileStatRow(Icons.menu_book_outlined, bookCount, '阅读'),
|
||||
const SizedBox(height: 12),
|
||||
_buildProfileStatRow(Icons.note_outlined, noteCount, '笔记'),
|
||||
|
||||
// 设置入口
|
||||
const SizedBox(height: 14),
|
||||
const Divider(height: 1, color: Color(0xFFF0F0F0)),
|
||||
Divider(height: 1, color: colors.outlineVariant),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsPage()));
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(top: 14),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.settings_outlined, size: 16, color: Color(0xFF999999)),
|
||||
SizedBox(width: 8),
|
||||
Text('设置', style: TextStyle(fontSize: 13, color: Color(0xFF888888))),
|
||||
Spacer(),
|
||||
Icon(Icons.chevron_right, size: 14, color: Color(0xFFD0D0D0)),
|
||||
Icon(Icons.settings_outlined, size: 16, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
const SizedBox(width: 8),
|
||||
Text('设置', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||
const Spacer(),
|
||||
Icon(Icons.chevron_right, size: 14, color: colors.onSurface.withValues(alpha: 0.2)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -172,13 +154,14 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
}
|
||||
|
||||
Widget _buildProfileStatRow(IconData icon, int count, String label) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: const Color(0xFF888888)),
|
||||
Icon(icon, size: 16, color: colors.onSurface.withValues(alpha: 0.5)),
|
||||
const SizedBox(width: 10),
|
||||
Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF888888))),
|
||||
Text(label, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||
const Spacer(),
|
||||
Text(_formatCount(count), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text(_formatCount(count), style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -192,10 +175,11 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
// ─── 功能入口卡片 ────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildToolsCard(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
@@ -204,12 +188,12 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
Navigator.pop(context);
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage()));
|
||||
}, topRounded: true),
|
||||
const Divider(height: 1, indent: 52, endIndent: 20, color: Color(0xFFF0F0F0)),
|
||||
Divider(height: 1, indent: 52, endIndent: 20, color: colors.outlineVariant),
|
||||
_buildToolItem(Icons.label_outline, '标签管理', () {
|
||||
Navigator.pop(context);
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const TagManagementPage()));
|
||||
}),
|
||||
const Divider(height: 1, indent: 52, endIndent: 20, color: Color(0xFFF0F0F0)),
|
||||
Divider(height: 1, indent: 52, endIndent: 20, color: colors.outlineVariant),
|
||||
_buildToolItem(Icons.description_outlined, 'MD阅读', () {
|
||||
Navigator.pop(context);
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const MdReaderTabPage()));
|
||||
@@ -220,6 +204,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
}
|
||||
|
||||
Widget _buildToolItem(IconData icon, String title, VoidCallback onTap, {bool topRounded = false, bool bottomRounded = false}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.only(
|
||||
@@ -232,21 +217,22 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: const Color(0xFF555555)),
|
||||
Icon(icon, size: 20, color: colors.onSurface.withValues(alpha: 0.7)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))),
|
||||
const Icon(Icons.chevron_right, size: 16, color: Color(0xFFD0D0D0)),
|
||||
Expanded(child: Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface))),
|
||||
Icon(Icons.chevron_right, size: 16, color: colors.onSurface.withValues(alpha: 0.2)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 热力图(保持现状) ──────────────────────────────────────────────
|
||||
// ─── 热力图 ──────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildCalendarSection(BuildContext context) {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final Map<DateTime, int> dailyCounts = {};
|
||||
for (final movie in provider.movies.where((m) => !m.isDeleted)) {
|
||||
final date = DateTime(movie.createdAt.year, movie.createdAt.month, movie.createdAt.day);
|
||||
@@ -299,15 +285,15 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
|
||||
decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(16)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.calendar_today, size: 14, color: Color(0xFF999999)),
|
||||
SizedBox(width: 8),
|
||||
Text('热力图', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))),
|
||||
Icon(Icons.calendar_today, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
const SizedBox(width: 8),
|
||||
Text('热力图', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
@@ -324,7 +310,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
final label = keepWeeks.contains(week) ? monthLabels[week] : null;
|
||||
return SizedBox(
|
||||
width: cellSize + cellGap,
|
||||
child: label != null ? Text(label, style: const TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))) : null,
|
||||
child: label != null ? Text(label, style: TextStyle(fontSize: 9, color: colors.onSurface.withValues(alpha: 0.3))) : null,
|
||||
);
|
||||
}),
|
||||
),
|
||||
@@ -347,7 +333,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const Text('少', style: TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))),
|
||||
Text('少', style: TextStyle(fontSize: 9, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||
const SizedBox(width: 3),
|
||||
_legendCell(const Color(0xFFF0F0F0)),
|
||||
_legendCell(const Color(0xFFC8E6C9)),
|
||||
@@ -355,7 +341,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
_legendCell(const Color(0xFF2E7D32)),
|
||||
_legendCell(const Color(0xFF1B5E20)),
|
||||
const SizedBox(width: 3),
|
||||
const Text('多', style: TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))),
|
||||
Text('多', style: TextStyle(fontSize: 9, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -384,21 +370,22 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
Widget _buildRecentSection(BuildContext context) {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final recent = _getRecentItems(provider);
|
||||
if (recent.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
|
||||
decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(16)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.schedule, size: 14, color: Color(0xFF999999)),
|
||||
SizedBox(width: 8),
|
||||
Text('最近添加', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))),
|
||||
Icon(Icons.schedule, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
const SizedBox(width: 8),
|
||||
Text('最近添加', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
@@ -408,15 +395,15 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
children: [
|
||||
Icon(
|
||||
item.type == 'movie' ? Icons.movie_outlined : item.type == 'book' ? Icons.menu_book_outlined : Icons.note_outlined,
|
||||
size: 14, color: const Color(0xFFBBBBBB),
|
||||
size: 14, color: colors.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(item.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 13, color: Color(0xFF444444))),
|
||||
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.75))),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(_recentTimeAgo(item.date), style: const TextStyle(fontSize: 10, color: Color(0xFFCCCCCC))),
|
||||
Text(_recentTimeAgo(item.date), style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||
],
|
||||
),
|
||||
)),
|
||||
|
||||
@@ -51,7 +51,6 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是 http 开头,直接当网络图片
|
||||
if (widget.path!.startsWith('http')) {
|
||||
_useNetwork = true;
|
||||
_imageUrl = widget.path;
|
||||
@@ -60,7 +59,6 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
||||
return;
|
||||
}
|
||||
|
||||
// 本地文件存在就直接显示
|
||||
final file = File(widget.path!);
|
||||
if (file.existsSync()) {
|
||||
setState(() => _loaded = true);
|
||||
@@ -68,7 +66,6 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
||||
return;
|
||||
}
|
||||
|
||||
// 本地不存在,尝试服务端 URL
|
||||
if (ServerDataService.isActive) {
|
||||
try {
|
||||
final url = await ServerDataService.toImageUrl(widget.path!);
|
||||
@@ -105,13 +102,14 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
if (_error) {
|
||||
return widget.errorWidget ??
|
||||
Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
color: const Color(0xFFF5F5F5),
|
||||
child: const Icon(Icons.broken_image_outlined, size: 24, color: Color(0xFFCCCCCC)),
|
||||
color: colors.surfaceContainerHighest,
|
||||
child: Icon(Icons.broken_image_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
);
|
||||
}
|
||||
if (!_loaded) {
|
||||
@@ -119,7 +117,7 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
||||
Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
);
|
||||
}
|
||||
return FadeTransition(
|
||||
@@ -136,8 +134,8 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
||||
Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
color: const Color(0xFFF5F5F5),
|
||||
child: const Icon(Icons.broken_image_outlined, size: 24, color: Color(0xFFCCCCCC)),
|
||||
color: colors.surfaceContainerHighest,
|
||||
child: Icon(Icons.broken_image_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
);
|
||||
},
|
||||
)
|
||||
@@ -150,8 +148,8 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
||||
Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
color: const Color(0xFFF5F5F5),
|
||||
child: const Icon(Icons.broken_image_outlined, size: 24, color: Color(0xFFCCCCCC)),
|
||||
color: colors.surfaceContainerHighest,
|
||||
child: Icon(Icons.broken_image_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ class MovieListItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, '/movie-detail', arguments: movie);
|
||||
@@ -23,28 +24,21 @@ class MovieListItem extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 海报
|
||||
Expanded(
|
||||
child: _buildPoster(),
|
||||
child: _buildPoster(colors),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 影视名称
|
||||
Text(
|
||||
movie.title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// 评分
|
||||
if (movie.rating != null)
|
||||
AnimatedStarRating(rating: movie.rating!, starSize: 12, showNumber: true)
|
||||
else
|
||||
@@ -53,45 +47,45 @@ class MovieListItem extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建海报
|
||||
Widget _buildPoster() {
|
||||
|
||||
Widget _buildPoster(ColorScheme colors) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5),
|
||||
border: Border.all(color: colors.outline, width: 0.5),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: FadeInLocalImage(
|
||||
path: movie.posterPath,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: const Center(child: Icon(Icons.movie_outlined, size: 24, color: Color(0xFFCCCCCC))),
|
||||
errorWidget: const Center(child: Icon(Icons.movie_outlined, size: 24, color: Color(0xFFCCCCCC))),
|
||||
placeholder: Center(child: Icon(Icons.movie_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||
errorWidget: Center(child: Icon(Icons.movie_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示删除确认对话框
|
||||
|
||||
void _showDeleteDialog(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text(
|
||||
title: Text(
|
||||
'确认删除',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
content: Text(
|
||||
'确定要删除《${movie.title}》吗?删除后可在回收站恢复。',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -99,7 +93,7 @@ class MovieListItem extends StatelessWidget {
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF666666),
|
||||
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
child: const Text('取消'),
|
||||
@@ -111,8 +105,8 @@ class MovieListItem extends StatelessWidget {
|
||||
ToastUtil.show(context, '已删除');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colors.error,
|
||||
foregroundColor: colors.onError,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
|
||||
@@ -8,20 +8,21 @@ class MovieStatusBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
|
||||
bottom: BorderSide(color: colors.outline, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
@@ -42,7 +43,7 @@ class MovieStatusBar extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(3),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
color: colors.primary,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -58,18 +59,21 @@ class MovieStatusBar extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
_buildTab(
|
||||
colors: colors,
|
||||
label: '已看',
|
||||
icon: Icons.check_circle_outline,
|
||||
isSelected: provider.movieStatusIndex == 0,
|
||||
onTap: () => provider.setMovieStatusIndex(0),
|
||||
),
|
||||
_buildTab(
|
||||
colors: colors,
|
||||
label: '在看',
|
||||
icon: Icons.play_circle_outline,
|
||||
isSelected: provider.movieStatusIndex == 1,
|
||||
onTap: () => provider.setMovieStatusIndex(1),
|
||||
),
|
||||
_buildTab(
|
||||
colors: colors,
|
||||
label: '想看',
|
||||
icon: Icons.bookmark_outline,
|
||||
isSelected: provider.movieStatusIndex == 2,
|
||||
@@ -89,6 +93,7 @@ class MovieStatusBar extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildTab({
|
||||
required ColorScheme colors,
|
||||
required String label,
|
||||
required IconData icon,
|
||||
required bool isSelected,
|
||||
@@ -108,7 +113,7 @@ class MovieStatusBar extends StatelessWidget {
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: isSelected ? Colors.white : const Color(0xFF666666),
|
||||
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
@@ -116,7 +121,7 @@ class MovieStatusBar extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: isSelected ? Colors.white : const Color(0xFF666666),
|
||||
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -14,14 +14,12 @@ class NoteListItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 使用 RepaintBoundary 减少重绘
|
||||
return RepaintBoundary(
|
||||
child: _NoteListItemContent(note: note),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 笔记列表项内容 - 分离出来便于优化
|
||||
class _NoteListItemContent extends StatelessWidget {
|
||||
final Note note;
|
||||
|
||||
@@ -29,10 +27,10 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) async {
|
||||
// 返回时刷新笔记列表
|
||||
await context.read<AppProvider>().loadNotes();
|
||||
});
|
||||
},
|
||||
@@ -41,40 +39,37 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFA),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 顶部:时间 + MD标记
|
||||
Row(
|
||||
children: [
|
||||
// 时间
|
||||
Text(
|
||||
_formatDateCached(note.updatedAt),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// MD标记
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'MD',
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF999999),
|
||||
color: colors.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -83,14 +78,13 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// 标题
|
||||
if (note.title.isNotEmpty) ...[
|
||||
Text(
|
||||
note.title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1A1A),
|
||||
color: colors.onSurface,
|
||||
height: 1.4,
|
||||
),
|
||||
maxLines: 1,
|
||||
@@ -99,20 +93,18 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
|
||||
// 内容摘要(去除Markdown标记),内容为空则不显示
|
||||
if (_collapseBlankLines(_cleanMarkdown(note.content).trim()).isNotEmpty)
|
||||
Text(
|
||||
_collapseBlankLines(_cleanMarkdown(note.content).trim()),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
height: 1.5,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
// 底部标签
|
||||
if (note.tags.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
@@ -122,15 +114,15 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -138,10 +130,9 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
|
||||
// 图片预览
|
||||
if (note.images.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildImagePreviewRow(),
|
||||
_buildImagePreviewRow(colors),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -149,8 +140,7 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 图片预览行
|
||||
Widget _buildImagePreviewRow() {
|
||||
Widget _buildImagePreviewRow(ColorScheme colors) {
|
||||
final images = note.images;
|
||||
final count = images.length.clamp(0, 3);
|
||||
return Row(
|
||||
@@ -162,14 +152,14 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: FadeInLocalImage(
|
||||
path: images[i],
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: Container(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -178,13 +168,13 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'+${images.length - 3}',
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF999999), fontWeight: FontWeight.w500),
|
||||
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4), fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -192,45 +182,44 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 清理 Markdown 标记,提取纯文本
|
||||
String _cleanMarkdown(String text) {
|
||||
return text
|
||||
.replaceAll(RegExp(r'^#+\s+', multiLine: true), '') // 标题
|
||||
.replaceAll(RegExp(r'\*\*(.+?)\*\*'), r'$1') // 粗体
|
||||
.replaceAll(RegExp(r'\*(.+?)\*'), r'$1') // 斜体
|
||||
.replaceAll(RegExp(r'`(.+?)`'), r'$1') // 行内代码
|
||||
.replaceAll(RegExp(r'^\s*[-*+]\s', multiLine: true), '') // 列表
|
||||
.replaceAll(RegExp(r'^\s*>\s', multiLine: true), '') // 引用
|
||||
.replaceAll(RegExp(r'\[([^\]]+)\]\([^)]+\)'), r'$1') // 链接
|
||||
.replaceAll(RegExp(r'!\[([^\]]*)\]\([^)]+\)'), '') // 图片
|
||||
.replaceAll(RegExp(r'^#+\s+', multiLine: true), '')
|
||||
.replaceAll(RegExp(r'\*\*(.+?)\*\*'), r'$1')
|
||||
.replaceAll(RegExp(r'\*(.+?)\*'), r'$1')
|
||||
.replaceAll(RegExp(r'`(.+?)`'), r'$1')
|
||||
.replaceAll(RegExp(r'^\s*[-*+]\s', multiLine: true), '')
|
||||
.replaceAll(RegExp(r'^\s*>\s', multiLine: true), '')
|
||||
.replaceAll(RegExp(r'\[([^\]]+)\]\([^)]+\)'), r'$1')
|
||||
.replaceAll(RegExp(r'!\[([^\]]*)\]\([^)]+\)'), '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/// 合并连续空行为单行
|
||||
String _collapseBlankLines(String text) {
|
||||
return text.replaceAll(RegExp(r'\n\s*\n+'), '\n');
|
||||
}
|
||||
|
||||
/// 显示删除确认对话框
|
||||
void _showDeleteDialog(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text(
|
||||
title: Text(
|
||||
'确认删除',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
content: const Text(
|
||||
content: Text(
|
||||
'确定要删除这条笔记吗?删除后可在回收站恢复。',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF666666),
|
||||
color: colors.onSurface.withValues(alpha: 0.6),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -238,7 +227,7 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF666666),
|
||||
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
child: const Text('取消'),
|
||||
@@ -250,8 +239,8 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
ToastUtil.show(context, '已删除');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colors.error,
|
||||
foregroundColor: colors.onError,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -267,28 +256,24 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// 日期格式化缓存
|
||||
final Map<DateTime, String> _dateFormatCache = {};
|
||||
|
||||
/// 格式化日期(带缓存)
|
||||
String _formatDateCached(DateTime date) {
|
||||
// 使用日期部分作为缓存键(忽略时分秒)
|
||||
final cacheKey = DateTime(date.year, date.month, date.day);
|
||||
|
||||
|
||||
if (_dateFormatCache.containsKey(cacheKey)) {
|
||||
return _dateFormatCache[cacheKey]!;
|
||||
}
|
||||
|
||||
|
||||
final result = _formatDate(date);
|
||||
_dateFormatCache[cacheKey] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 格式化日期
|
||||
String _formatDate(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(date);
|
||||
|
||||
|
||||
if (difference.inDays == 0) {
|
||||
if (difference.inHours == 0) {
|
||||
if (difference.inMinutes == 0) {
|
||||
|
||||
@@ -42,6 +42,7 @@ class _ShimmerSkeletonState extends State<ShimmerSkeleton>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
@@ -49,7 +50,7 @@ class _ShimmerSkeletonState extends State<ShimmerSkeleton>
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE0E0E0).withValues(alpha: _animation.value),
|
||||
color: colors.surfaceContainerHighest.withValues(alpha: _animation.value),
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||
),
|
||||
);
|
||||
@@ -134,6 +135,7 @@ class NoteSkeletonList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 100),
|
||||
itemCount: 6,
|
||||
@@ -141,7 +143,7 @@ class NoteSkeletonList extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F8F8),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Column(
|
||||
|
||||
Reference in New Issue
Block a user