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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'dart:async';
|
|
||||||
import 'pages/home_page.dart';
|
import 'pages/home_page.dart';
|
||||||
import 'utils/theme/app_theme.dart';
|
import 'utils/theme/app_theme.dart';
|
||||||
import 'utils/app_router.dart';
|
import 'utils/app_router.dart';
|
||||||
@@ -13,31 +14,17 @@ import 'providers/app_provider.dart';
|
|||||||
import 'package:flutter/widget_previews.dart';
|
import 'package:flutter/widget_previews.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
// 确保 Flutter 绑定初始化完成
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
// 设置系统导航栏颜色(与App主题一致)
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
|
||||||
const SystemUiOverlayStyle(
|
|
||||||
systemNavigationBarColor: Colors.white,
|
|
||||||
systemNavigationBarIconBrightness: Brightness.dark,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 初始化用户偏好设置
|
|
||||||
await UserPrefs.init();
|
await UserPrefs.init();
|
||||||
final appProvider = AppProvider();
|
final appProvider = AppProvider();
|
||||||
// 先显示界面,后台加载数据
|
|
||||||
runApp(MyApp(appProvider: appProvider));
|
runApp(MyApp(appProvider: appProvider));
|
||||||
unawaited(appProvider.initDatabase().then((_) => appProvider.initMainTabIndex()));
|
unawaited(appProvider.initDatabase().then((_) => appProvider.initMainTabIndex()));
|
||||||
unawaited(_initAutoBackup());
|
unawaited(_initAutoBackup());
|
||||||
unawaited(_initUsageStats());
|
unawaited(_initUsageStats());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 初始化自动备份
|
|
||||||
Future<void> _initAutoBackup() async {
|
Future<void> _initAutoBackup() async {
|
||||||
try {
|
try {
|
||||||
// 初始化本地自动备份
|
|
||||||
final isLocalAutoBackupEnabled = await AutoBackupService.instance.getEnabled();
|
final isLocalAutoBackupEnabled = await AutoBackupService.instance.getEnabled();
|
||||||
if (isLocalAutoBackupEnabled) {
|
if (isLocalAutoBackupEnabled) {
|
||||||
await AutoBackupService.instance.start();
|
await AutoBackupService.instance.start();
|
||||||
@@ -47,7 +34,6 @@ Future<void> _initAutoBackup() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 初始化匿名用户统计
|
|
||||||
Future<void> _initUsageStats() async {
|
Future<void> _initUsageStats() async {
|
||||||
try {
|
try {
|
||||||
await UsageStatsService.instance.start();
|
await UsageStatsService.instance.start();
|
||||||
@@ -56,51 +42,92 @@ Future<void> _initUsageStats() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatefulWidget {
|
||||||
final AppProvider appProvider;
|
final AppProvider appProvider;
|
||||||
|
|
||||||
const MyApp({super.key, required this.appProvider});
|
const MyApp({super.key, required this.appProvider});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyApp> createState() => _MyAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
widget.appProvider.loadThemeMode();
|
||||||
|
_updateSystemUI(widget.appProvider.themeMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
_updateSystemUI(widget.appProvider.themeMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateSystemUI(ThemeMode mode) {
|
||||||
|
final Brightness brightness;
|
||||||
|
switch (mode) {
|
||||||
|
case ThemeMode.light:
|
||||||
|
brightness = Brightness.light;
|
||||||
|
case ThemeMode.dark:
|
||||||
|
brightness = Brightness.dark;
|
||||||
|
case ThemeMode.system:
|
||||||
|
brightness = PlatformDispatcher.instance.platformBrightness;
|
||||||
|
}
|
||||||
|
final isDark = brightness == Brightness.dark;
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor: isDark ? const Color(0xFF1A1A1A) : Colors.white,
|
||||||
|
systemNavigationBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// 获取当前选中的图标名称
|
|
||||||
final iconName = UserPrefs().appIconName;
|
final iconName = UserPrefs().appIconName;
|
||||||
|
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider.value(value: appProvider),
|
ChangeNotifierProvider.value(value: widget.appProvider),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: Consumer<AppProvider>(
|
||||||
title: 'MookNote',
|
builder: (context, provider, _) {
|
||||||
debugShowCheckedModeBanner: false,
|
_updateSystemUI(provider.themeMode);
|
||||||
theme: AppTheme.lightTheme,
|
return MaterialApp(
|
||||||
darkTheme: AppTheme.darkTheme,
|
title: 'MookNote',
|
||||||
themeMode: ThemeMode.system,
|
debugShowCheckedModeBanner: false,
|
||||||
localizationsDelegates: [
|
theme: AppTheme.lightTheme,
|
||||||
GlobalMaterialLocalizations.delegate,
|
darkTheme: AppTheme.darkTheme,
|
||||||
GlobalWidgetsLocalizations.delegate,
|
themeMode: provider.themeMode,
|
||||||
GlobalCupertinoLocalizations.delegate,
|
localizationsDelegates: [
|
||||||
],
|
GlobalMaterialLocalizations.delegate,
|
||||||
supportedLocales: const [
|
GlobalWidgetsLocalizations.delegate,
|
||||||
Locale('zh', 'CN'),
|
GlobalCupertinoLocalizations.delegate,
|
||||||
Locale('en', 'US'),
|
],
|
||||||
],
|
supportedLocales: const [
|
||||||
home: const HomePage(),
|
Locale('zh', 'CN'),
|
||||||
onGenerateRoute: AppRouter.generateRoute,
|
Locale('en', 'US'),
|
||||||
builder: (context, child) {
|
],
|
||||||
// 尝试动态设置应用图标(Android 13+ 支持动态图标,但 Flutter 目前主要通过静态配置)
|
home: const HomePage(),
|
||||||
// 这里我们主要实现逻辑上的切换,实际生效通常需要重启应用或配合原生插件
|
onGenerateRoute: AppRouter.generateRoute,
|
||||||
return _AppIconWrapper(iconName: iconName, child: child!);
|
builder: (context, child) {
|
||||||
|
return _AppIconWrapper(iconName: iconName, child: child!);
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 应用图标包装器
|
|
||||||
/// 注意:Flutter 默认不支持运行时动态更换桌面图标。
|
|
||||||
/// 这里的实现主要是为了在应用内记录用户的选择,并为未来可能的动态图标功能做准备。
|
|
||||||
/// 如果需要真正的动态图标,通常需要引入 flutter_app_icon_changer 等插件并配置多套图标资源。
|
|
||||||
class _AppIconWrapper extends StatefulWidget {
|
class _AppIconWrapper extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final String iconName;
|
final String iconName;
|
||||||
@@ -118,11 +145,7 @@ class _AppIconWrapperState extends State<_AppIconWrapper> {
|
|||||||
_updateSystemIcon();
|
_updateSystemIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateSystemIcon() async {
|
Future<void> _updateSystemIcon() async {}
|
||||||
// 目前 Flutter 官方不支持直接通过代码更换 Launcher Icon。
|
|
||||||
// 这一步主要用于记录日志或在未来集成第三方库时使用。
|
|
||||||
// print('Current selected icon: ${widget.iconName}');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -130,11 +153,9 @@ class _AppIconWrapperState extends State<_AppIconWrapper> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 用于预览 MyApp 的 Widget
|
|
||||||
/// 添加 @Preview 注解
|
|
||||||
@Preview(name: "MookNote App Preview")
|
@Preview(name: "MookNote App Preview")
|
||||||
Widget previewMyApp() {
|
Widget previewMyApp() {
|
||||||
final appProvider = AppProvider();
|
final appProvider = AppProvider();
|
||||||
|
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
@@ -142,4 +163,4 @@ Widget previewMyApp() {
|
|||||||
],
|
],
|
||||||
child: MyApp(appProvider: appProvider),
|
child: MyApp(appProvider: appProvider),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
|||||||
final UserPrefs _userPrefs = UserPrefs();
|
final UserPrefs _userPrefs = UserPrefs();
|
||||||
String _currentIconName = 'app_icon';
|
String _currentIconName = 'app_icon';
|
||||||
|
|
||||||
// 预定义的图标列表
|
|
||||||
final List<Map<String, String>> _icons = [
|
final List<Map<String, String>> _icons = [
|
||||||
{'name': 'app_icon', 'label': '默认图标'},
|
{'name': 'app_icon', 'label': '默认图标'},
|
||||||
{'name': 'app_icon2', 'label': '风格二'},
|
{'name': 'app_icon2', 'label': '风格二'},
|
||||||
@@ -28,7 +27,6 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadCurrentIcon() async {
|
Future<void> _loadCurrentIcon() async {
|
||||||
// 先从原生层获取当前实际启用的图标(更准确)
|
|
||||||
final nativeIcon = await AppIconChannel.getCurrentIcon();
|
final nativeIcon = await AppIconChannel.getCurrentIcon();
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIconName = nativeIcon;
|
_currentIconName = nativeIcon;
|
||||||
@@ -39,7 +37,6 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
|||||||
if (iconName == _currentIconName) return;
|
if (iconName == _currentIconName) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用原生层切换桌面图标
|
|
||||||
final success = await AppIconChannel.switchIcon(iconName);
|
final success = await AppIconChannel.switchIcon(iconName);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -65,8 +62,9 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('应用图标'),
|
title: const Text('应用图标'),
|
||||||
),
|
),
|
||||||
@@ -82,22 +80,21 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final icon = _icons[index];
|
final icon = _icons[index];
|
||||||
final isSelected = _currentIconName == icon['name'];
|
final isSelected = _currentIconName == icon['name'];
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => _selectIcon(icon['name']!),
|
onTap: () => _selectIcon(icon['name']!),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? const Color(0xFFF0F0F0) : const Color(0xFFFAFAFA),
|
color: isSelected ? colors.outlineVariant : colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFE8E8E8),
|
color: isSelected ? colors.primary : colors.outlineVariant,
|
||||||
width: isSelected ? 2 : 1,
|
width: isSelected ? 2 : 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// 图标预览
|
|
||||||
Image.asset(
|
Image.asset(
|
||||||
'assets/icon/${icon['name']}.png',
|
'assets/icon/${icon['name']}.png',
|
||||||
width: 64,
|
width: 64,
|
||||||
@@ -107,26 +104,25 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
|||||||
width: 64,
|
width: 64,
|
||||||
height: 64,
|
height: 64,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFEEEEEE),
|
color: colors.outlineVariant,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.image_not_supported, color: Color(0xFF999999)),
|
child: Icon(Icons.image_not_supported, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// 标签
|
|
||||||
Text(
|
Text(
|
||||||
icon['label']!,
|
icon['label']!,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isSelected) ...[
|
if (isSelected) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
const Icon(Icons.check_circle, size: 18, color: Color(0xFF1A1A1A)),
|
Icon(Icons.check_circle, size: 18, color: colors.primary),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import 'book_share_page.dart';
|
|||||||
/// 书籍详情页 - 极简主义设计
|
/// 书籍详情页 - 极简主义设计
|
||||||
class BookDetailPage extends StatefulWidget {
|
class BookDetailPage extends StatefulWidget {
|
||||||
final Book book;
|
final Book book;
|
||||||
|
|
||||||
const BookDetailPage({super.key, required this.book});
|
const BookDetailPage({super.key, required this.book});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<BookDetailPage> createState() => _BookDetailPageState();
|
State<BookDetailPage> createState() => _BookDetailPageState();
|
||||||
}
|
}
|
||||||
@@ -24,80 +24,54 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
// 页面获得焦点时刷新数据
|
|
||||||
_refreshBookData();
|
_refreshBookData();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _refreshBookData() {
|
void _refreshBookData() {
|
||||||
final provider = context.read<AppProvider>();
|
final provider = context.read<AppProvider>();
|
||||||
// 强制刷新当前书籍数据
|
|
||||||
provider.loadBooks();
|
provider.loadBooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// 从 Provider 获取最新的 book 数据,实现动态刷新
|
final colors = Theme.of(context).colorScheme;
|
||||||
final book = context.watch<AppProvider>().books
|
final book = context.watch<AppProvider>().books
|
||||||
.where((b) => b.id == widget.book.id)
|
.where((b) => b.id == widget.book.id)
|
||||||
.firstOrNull ?? widget.book;
|
.firstOrNull ?? widget.book;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
CustomScrollView(
|
CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
// 顶部封面区域
|
|
||||||
_buildSliverAppBar(book),
|
_buildSliverAppBar(book),
|
||||||
|
|
||||||
// 内容区域
|
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 基本信息
|
|
||||||
_buildBasicInfo(book),
|
_buildBasicInfo(book),
|
||||||
|
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
|
||||||
|
|
||||||
// 作者信息
|
|
||||||
_buildAuthorsSection(book),
|
_buildAuthorsSection(book),
|
||||||
|
|
||||||
// 类型
|
|
||||||
if (book.genres.isNotEmpty)
|
if (book.genres.isNotEmpty)
|
||||||
_buildGenresSection(book),
|
_buildGenresSection(book),
|
||||||
|
|
||||||
// ISBN
|
|
||||||
if (book.isbn != null && book.isbn!.isNotEmpty)
|
if (book.isbn != null && book.isbn!.isNotEmpty)
|
||||||
_buildIsbnSection(book),
|
_buildIsbnSection(book),
|
||||||
|
|
||||||
// 出版社
|
|
||||||
if (book.publisher != null && book.publisher!.isNotEmpty)
|
if (book.publisher != null && book.publisher!.isNotEmpty)
|
||||||
_buildPublisherSection(book),
|
_buildPublisherSection(book),
|
||||||
|
|
||||||
// 出版时间
|
|
||||||
if (book.publishDate != null)
|
if (book.publishDate != null)
|
||||||
_buildPublishDateSection(book),
|
_buildPublishDateSection(book),
|
||||||
|
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
|
||||||
|
|
||||||
// 简介
|
|
||||||
if (book.summary != null && book.summary!.isNotEmpty)
|
if (book.summary != null && book.summary!.isNotEmpty)
|
||||||
_buildSummarySection(book),
|
_buildSummarySection(book),
|
||||||
|
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
|
||||||
|
|
||||||
// 书评和摘抄入口
|
|
||||||
_buildExtraSections(book),
|
_buildExtraSections(book),
|
||||||
|
|
||||||
const SizedBox(height: 120),
|
const SizedBox(height: 120),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// 右下角悬浮按钮组
|
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 16,
|
right: 16,
|
||||||
bottom: 24,
|
bottom: 24,
|
||||||
@@ -107,9 +81,9 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建右下角悬浮按钮组
|
|
||||||
Widget _buildFloatingActionButtons(Book book) {
|
Widget _buildFloatingActionButtons(Book book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -117,13 +91,16 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
icon: Icons.edit_outlined,
|
icon: Icons.edit_outlined,
|
||||||
onPressed: () => _navigateToEdit(context),
|
onPressed: () => _navigateToEdit(context),
|
||||||
tooltip: '编辑',
|
tooltip: '编辑',
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
foregroundColor: colors.onPrimary,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildFloatingButton(
|
_buildFloatingButton(
|
||||||
icon: Icons.delete_outline,
|
icon: Icons.delete_outline,
|
||||||
onPressed: () => _showDeleteDialog(context),
|
onPressed: () => _showDeleteDialog(context),
|
||||||
tooltip: '删除',
|
tooltip: '删除',
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: colors.error,
|
||||||
|
foregroundColor: colors.onError,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildFloatingButton(
|
_buildFloatingButton(
|
||||||
@@ -131,17 +108,18 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
onPressed: () => _showSharePoster(book),
|
onPressed: () => _showSharePoster(book),
|
||||||
tooltip: '分享海报',
|
tooltip: '分享海报',
|
||||||
backgroundColor: const Color(0xFF4CAF50),
|
backgroundColor: const Color(0xFF4CAF50),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建单个悬浮按钮
|
|
||||||
Widget _buildFloatingButton({
|
Widget _buildFloatingButton({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required VoidCallback onPressed,
|
required VoidCallback onPressed,
|
||||||
required String tooltip,
|
required String tooltip,
|
||||||
Color backgroundColor = const Color(0xFF1A1A1A),
|
required Color backgroundColor,
|
||||||
|
required Color foregroundColor,
|
||||||
}) {
|
}) {
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
@@ -160,15 +138,15 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(icon, size: 18, color: Colors.white),
|
icon: Icon(icon, size: 18, color: foregroundColor),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
tooltip: tooltip,
|
tooltip: tooltip,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建带背景的返回按钮
|
|
||||||
Widget _buildBackButton() {
|
Widget _buildBackButton() {
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||||
@@ -194,21 +172,19 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建顶部 AppBar
|
|
||||||
Widget _buildSliverAppBar(Book book) {
|
Widget _buildSliverAppBar(Book book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
expandedHeight: 320,
|
expandedHeight: 320,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: colors.surfaceContainerHighest,
|
||||||
leading: _buildBackButton(),
|
leading: _buildBackButton(),
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
background: _buildCoverSection(book),
|
background: _buildCoverSection(book),
|
||||||
),
|
),
|
||||||
// 右上角按钮已移到右下角悬浮按钮
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建封面区域
|
|
||||||
Widget _buildCoverSection(Book book) {
|
Widget _buildCoverSection(Book book) {
|
||||||
return SizedBox.expand(
|
return SizedBox.expand(
|
||||||
child: book.coverPath != null && book.coverPath!.isNotEmpty
|
child: book.coverPath != null && book.coverPath!.isNotEmpty
|
||||||
@@ -220,79 +196,74 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
: _buildCoverPlaceholder(),
|
: _buildCoverPlaceholder(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCoverPlaceholder() {
|
Widget _buildCoverPlaceholder() {
|
||||||
return const Center(
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.menu_book,
|
Icons.menu_book,
|
||||||
size: 64,
|
size: 64,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'暂无封面',
|
'暂无封面',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建基本信息
|
|
||||||
Widget _buildBasicInfo(Book book) {
|
Widget _buildBasicInfo(Book book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 书名
|
|
||||||
Text(
|
Text(
|
||||||
book.title,
|
book.title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.3,
|
height: 1.3,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 别名(显示在主名称下面,用 / 分隔)
|
|
||||||
if (book.alternateTitles.isNotEmpty) ...[
|
if (book.alternateTitles.isNotEmpty) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
book.alternateTitles.join(' / '),
|
book.alternateTitles.join(' / '),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 评分和状态
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (book.rating != null) ...[
|
if (book.rating != null) ...[
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.star,
|
Icons.star,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
book.rating!.toStringAsFixed(1),
|
book.rating!.toStringAsFixed(1),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
@@ -300,15 +271,12 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
_buildStatusTag(book),
|
_buildStatusTag(book),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// 时间信息
|
|
||||||
Text(
|
Text(
|
||||||
'添加于 ${_formatDate(book.createdAt)}',
|
'添加于 ${_formatDate(book.createdAt)}',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -316,33 +284,33 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建状态标签
|
|
||||||
Widget _buildStatusTag(Book book) {
|
Widget _buildStatusTag(Book book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
String label;
|
String label;
|
||||||
Color bgColor;
|
Color bgColor;
|
||||||
Color textColor;
|
Color textColor;
|
||||||
switch (book.status) {
|
switch (book.status) {
|
||||||
case 'read':
|
case 'read':
|
||||||
label = '已读';
|
label = '已读';
|
||||||
bgColor = const Color(0xFF1A1A1A);
|
bgColor = colors.primary;
|
||||||
textColor = Colors.white;
|
textColor = colors.onPrimary;
|
||||||
break;
|
break;
|
||||||
case 'reading':
|
case 'reading':
|
||||||
label = '在读';
|
label = '在读';
|
||||||
bgColor = const Color(0xFFF0F0F0);
|
bgColor = colors.outlineVariant;
|
||||||
textColor = const Color(0xFF666666);
|
textColor = colors.onSurface.withValues(alpha: 0.6);
|
||||||
break;
|
break;
|
||||||
case 'want_to_read':
|
case 'want_to_read':
|
||||||
label = '想读';
|
label = '想读';
|
||||||
bgColor = const Color(0xFFF5F5F5);
|
bgColor = colors.surfaceContainerHighest;
|
||||||
textColor = const Color(0xFF999999);
|
textColor = colors.onSurface.withValues(alpha: 0.4);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
label = '未知';
|
label = '未知';
|
||||||
bgColor = const Color(0xFFEEEEEE);
|
bgColor = colors.outlineVariant;
|
||||||
textColor = const Color(0xFFCCCCCC);
|
textColor = colors.onSurface.withValues(alpha: 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -359,30 +327,30 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建作者区域
|
|
||||||
Widget _buildAuthorsSection(Book book) {
|
Widget _buildAuthorsSection(Book book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 64,
|
width: 64,
|
||||||
child: Text(
|
child: Text(
|
||||||
'作者',
|
'作者',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
book.authors.join(','),
|
book.authors.join(','),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -392,29 +360,29 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建ISBN区域
|
|
||||||
Widget _buildIsbnSection(Book book) {
|
Widget _buildIsbnSection(Book book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 64,
|
width: 64,
|
||||||
child: Text(
|
child: Text(
|
||||||
'ISBN',
|
'ISBN',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
book.isbn!,
|
book.isbn!,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -424,29 +392,29 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建出版社区域
|
|
||||||
Widget _buildPublisherSection(Book book) {
|
Widget _buildPublisherSection(Book book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 64,
|
width: 64,
|
||||||
child: Text(
|
child: Text(
|
||||||
'出版社',
|
'出版社',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
book.publisher!,
|
book.publisher!,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -456,29 +424,29 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建出版时间区域
|
|
||||||
Widget _buildPublishDateSection(Book book) {
|
Widget _buildPublishDateSection(Book book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 64,
|
width: 64,
|
||||||
child: Text(
|
child: Text(
|
||||||
'出版时间',
|
'出版时间',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'${book.publishDate!.year}年${book.publishDate!.month.toString().padLeft(2, '0')}月',
|
'${book.publishDate!.year}年${book.publishDate!.month.toString().padLeft(2, '0')}月',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -488,20 +456,20 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建类型区域
|
|
||||||
Widget _buildGenresSection(Book book) {
|
Widget _buildGenresSection(Book book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 64,
|
width: 64,
|
||||||
child: Text(
|
child: Text(
|
||||||
'类型',
|
'类型',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -513,14 +481,14 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
genre,
|
genre,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -531,9 +499,9 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建简介区域
|
|
||||||
Widget _buildSummarySection(Book book) {
|
Widget _buildSummarySection(Book book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -545,17 +513,17 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
width: 4,
|
width: 4,
|
||||||
height: 16,
|
height: 16,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text(
|
Text(
|
||||||
'简介',
|
'简介',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -564,14 +532,14 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
book.summary!,
|
book.summary!,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.8,
|
height: 1.8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -581,8 +549,8 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建额外功能区域(书评、摘抄)
|
|
||||||
Widget _buildExtraSections(Book book) {
|
Widget _buildExtraSections(Book book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -594,23 +562,22 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
width: 4,
|
width: 4,
|
||||||
height: 16,
|
height: 16,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text(
|
Text(
|
||||||
'更多',
|
'更多',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 书评入口
|
|
||||||
_buildExtraSectionItem(
|
_buildExtraSectionItem(
|
||||||
icon: Icons.rate_review_outlined,
|
icon: Icons.rate_review_outlined,
|
||||||
title: '书评',
|
title: '书评',
|
||||||
@@ -620,7 +587,6 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
onTap: () => _navigateToReviews(book),
|
onTap: () => _navigateToReviews(book),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// 摘抄入口
|
|
||||||
_buildExtraSectionItem(
|
_buildExtraSectionItem(
|
||||||
icon: Icons.format_quote_outlined,
|
icon: Icons.format_quote_outlined,
|
||||||
title: '摘抄',
|
title: '摘抄',
|
||||||
@@ -634,7 +600,6 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建更多区域项
|
|
||||||
Widget _buildExtraSectionItem({
|
Widget _buildExtraSectionItem({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String title,
|
required String title,
|
||||||
@@ -643,14 +608,15 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
required String unit,
|
required String unit,
|
||||||
required VoidCallback onTap,
|
required VoidCallback onTap,
|
||||||
}) {
|
}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -658,14 +624,14 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: const Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@@ -675,10 +641,10 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
@@ -688,9 +654,9 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
final count = snapshot.data ?? 0;
|
final count = snapshot.data ?? 0;
|
||||||
return Text(
|
return Text(
|
||||||
count > 0 ? '$count $unit' : emptyText,
|
count > 0 ? '$count $unit' : emptyText,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -698,9 +664,9 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.chevron_right,
|
Icons.chevron_right,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -726,39 +692,37 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 格式化日期
|
|
||||||
String _formatDate(DateTime date) {
|
String _formatDate(DateTime date) {
|
||||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 跳转到编辑页面
|
|
||||||
void _navigateToEdit(BuildContext context) {
|
void _navigateToEdit(BuildContext context) {
|
||||||
Navigator.pushNamed(context, '/book-form', arguments: widget.book).then((_) {
|
Navigator.pushNamed(context, '/book-form', arguments: widget.book).then((_) {
|
||||||
context.read<AppProvider>().loadBooks();
|
context.read<AppProvider>().loadBooks();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示删除对话框
|
|
||||||
void _showDeleteDialog(BuildContext context) {
|
void _showDeleteDialog(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'确认删除',
|
'确认删除',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
content: Text(
|
content: Text(
|
||||||
'确定要删除"${widget.book.title}"吗?删除后可在回收站恢复。',
|
'确定要删除"${widget.book.title}"吗?删除后可在回收站恢复。',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -766,7 +730,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFF666666),
|
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
child: const Text('取消'),
|
child: const Text('取消'),
|
||||||
@@ -780,8 +744,8 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
ToastUtil.show(context, '已删除');
|
ToastUtil.show(context, '已删除');
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: colors.error,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colors.onError,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -796,7 +760,6 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 下载封面到本地
|
|
||||||
Future<void> _downloadCover(Book book) async {
|
Future<void> _downloadCover(Book book) async {
|
||||||
if (book.coverPath == null || book.coverPath!.isEmpty) {
|
if (book.coverPath == null || book.coverPath!.isEmpty) {
|
||||||
ToastUtil.show(context, '没有可下载的封面');
|
ToastUtil.show(context, '没有可下载的封面');
|
||||||
@@ -810,16 +773,13 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成文件名:书籍名称_时间戳_封面.jpg
|
|
||||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
final fileName = '${book.title}_${timestamp}_封面.jpg';
|
final fileName = '${book.title}_${timestamp}_封面.jpg';
|
||||||
|
|
||||||
// 获取临时目录路径
|
|
||||||
final tempDir = await Directory.systemTemp.createTemp();
|
final tempDir = await Directory.systemTemp.createTemp();
|
||||||
final tempFile = File('${tempDir.path}/$fileName');
|
final tempFile = File('${tempDir.path}/$fileName');
|
||||||
await sourceFile.copy(tempFile.path);
|
await sourceFile.copy(tempFile.path);
|
||||||
|
|
||||||
// 使用分享功能让用户选择保存位置
|
|
||||||
await Share.shareXFiles(
|
await Share.shareXFiles(
|
||||||
[XFile(tempFile.path)],
|
[XFile(tempFile.path)],
|
||||||
subject: '${book.title} 封面',
|
subject: '${book.title} 封面',
|
||||||
@@ -830,7 +790,6 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示分享海报页面
|
|
||||||
void _showSharePoster(Book book) {
|
void _showSharePoster(Book book) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -50,8 +50,9 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(_isEditing ? '编辑摘抄' : '添加摘抄'),
|
title: Text(_isEditing ? '编辑摘抄' : '添加摘抄'),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -63,10 +64,10 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
|||||||
height: 20,
|
height: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Text(
|
: Text(
|
||||||
'保存',
|
'保存',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -82,15 +83,15 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
|||||||
// 章节
|
// 章节
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _chapterController,
|
controller: _chapterController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '章节(可选)',
|
labelText: '章节(可选)',
|
||||||
hintText: '例如:第一章、第3节等',
|
hintText: '例如:第一章、第3节等',
|
||||||
border: OutlineInputBorder(
|
border: const OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
borderSide: BorderSide(color: Color(0xFF1A1A1A)),
|
borderSide: BorderSide(color: colors.primary),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -101,16 +102,16 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _contentController,
|
controller: _contentController,
|
||||||
maxLines: 8,
|
maxLines: 8,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '摘抄内容',
|
labelText: '摘抄内容',
|
||||||
hintText: '输入你想要摘抄的内容...',
|
hintText: '输入你想要摘抄的内容...',
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
border: OutlineInputBorder(
|
border: const OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
borderSide: BorderSide(color: Color(0xFF1A1A1A)),
|
borderSide: BorderSide(color: colors.primary),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
@@ -127,16 +128,16 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _commentController,
|
controller: _commentController,
|
||||||
maxLines: 5,
|
maxLines: 5,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '我的感悟(可选)',
|
labelText: '我的感悟(可选)',
|
||||||
hintText: '记录你对这段内容的思考和感悟...',
|
hintText: '记录你对这段内容的思考和感悟...',
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
border: OutlineInputBorder(
|
border: const OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
borderSide: BorderSide(color: Color(0xFF1A1A1A)),
|
borderSide: BorderSide(color: colors.primary),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -185,4 +186,3 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('摘抄'),
|
title: const Text('摘抄'),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -62,6 +63,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState() {
|
Widget _buildEmptyState() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -70,21 +72,21 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.format_quote_outlined,
|
Icons.format_quote_outlined,
|
||||||
size: 40,
|
size: 40,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Text(
|
Text(
|
||||||
'暂无摘抄',
|
'暂无摘抄',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -93,15 +95,15 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'添加记录',
|
'添加记录',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Colors.white,
|
color: colors.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -114,7 +116,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
/// 按章节分组摘抄数据
|
/// 按章节分组摘抄数据
|
||||||
Map<String, List<BookExcerpt>> _groupExcerptsByChapter() {
|
Map<String, List<BookExcerpt>> _groupExcerptsByChapter() {
|
||||||
final Map<String, List<BookExcerpt>> groups = {};
|
final Map<String, List<BookExcerpt>> groups = {};
|
||||||
|
|
||||||
for (final excerpt in _excerpts) {
|
for (final excerpt in _excerpts) {
|
||||||
final chapter = excerpt.chapter.isEmpty ? '未分类' : excerpt.chapter;
|
final chapter = excerpt.chapter.isEmpty ? '未分类' : excerpt.chapter;
|
||||||
if (!groups.containsKey(chapter)) {
|
if (!groups.containsKey(chapter)) {
|
||||||
@@ -122,19 +124,19 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
}
|
}
|
||||||
groups[chapter]!.add(excerpt);
|
groups[chapter]!.add(excerpt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 每个章节内的摘抄按时间排序(新的在前)
|
// 每个章节内的摘抄按时间排序(新的在前)
|
||||||
for (final chapter in groups.keys) {
|
for (final chapter in groups.keys) {
|
||||||
groups[chapter]!.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
groups[chapter]!.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildExcerptList() {
|
Widget _buildExcerptList() {
|
||||||
final groups = _groupExcerptsByChapter();
|
final groups = _groupExcerptsByChapter();
|
||||||
final chapters = groups.keys.toList();
|
final chapters = groups.keys.toList();
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: chapters.length,
|
itemCount: chapters.length,
|
||||||
@@ -147,6 +149,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildChapterSection(String chapter, List<BookExcerpt> excerpts) {
|
Widget _buildChapterSection(String chapter, List<BookExcerpt> excerpts) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -154,18 +157,18 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
border: Border(
|
border: Border(
|
||||||
left: BorderSide(color: Color(0xFF1A1A1A), width: 4),
|
left: BorderSide(color: colors.primary, width: 4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
chapter,
|
chapter,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -178,11 +181,12 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildExcerptItem(BookExcerpt excerpt) {
|
Widget _buildExcerptItem(BookExcerpt excerpt) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
border: Border.all(color: colors.outline),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -192,9 +196,9 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
excerpt.content,
|
excerpt.content,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -205,16 +209,16 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
excerpt.comment,
|
excerpt.comment,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -227,29 +231,29 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_formatDate(excerpt.createdAt),
|
_formatDate(excerpt.createdAt),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
// 编辑按钮
|
// 编辑按钮
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => _navigateToEditExcerpt(excerpt),
|
onTap: () => _navigateToEditExcerpt(excerpt),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.edit_outlined,
|
Icons.edit_outlined,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
// 删除按钮
|
// 删除按钮
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => _showDeleteDialog(excerpt),
|
onTap: () => _showDeleteDialog(excerpt),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.delete_outline,
|
Icons.delete_outline,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Colors.red,
|
color: colors.error,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -287,28 +291,31 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
void _showDeleteDialog(BookExcerpt excerpt) {
|
void _showDeleteDialog(BookExcerpt excerpt) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) {
|
||||||
backgroundColor: Colors.white,
|
final colors = Theme.of(context).colorScheme;
|
||||||
elevation: 0,
|
return AlertDialog(
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
backgroundColor: colors.surface,
|
||||||
title: const Text('确认删除'),
|
elevation: 0,
|
||||||
content: const Text('确定要删除这条摘抄吗?'),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
actions: [
|
title: const Text('确认删除'),
|
||||||
TextButton(
|
content: const Text('确定要删除这条摘抄吗?'),
|
||||||
onPressed: () => Navigator.pop(context),
|
actions: [
|
||||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
TextButton(
|
||||||
),
|
onPressed: () => Navigator.pop(context),
|
||||||
TextButton(
|
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
onPressed: () async {
|
),
|
||||||
await context.read<AppProvider>().removeBookExcerpt(excerpt.id);
|
TextButton(
|
||||||
Navigator.pop(context);
|
onPressed: () async {
|
||||||
_loadExcerpts();
|
await context.read<AppProvider>().removeBookExcerpt(excerpt.id);
|
||||||
ToastUtil.show(context, '已删除');
|
Navigator.pop(context);
|
||||||
},
|
_loadExcerpts();
|
||||||
child: const Text('删除', style: TextStyle(color: Colors.red)),
|
ToastUtil.show(context, '已删除');
|
||||||
),
|
},
|
||||||
],
|
child: Text('删除', style: TextStyle(color: colors.error)),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -49,8 +49,9 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('书评详情'),
|
title: const Text('书评详情'),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -70,9 +71,9 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
|||||||
// 书评内容
|
// 书评内容
|
||||||
Text(
|
Text(
|
||||||
_review.content,
|
_review.content,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.8,
|
height: 1.8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -82,7 +83,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
|||||||
// 分隔线
|
// 分隔线
|
||||||
Container(
|
Container(
|
||||||
height: 0.5,
|
height: 0.5,
|
||||||
color: const Color(0xFFE5E5E5),
|
color: colors.outline,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -92,6 +93,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
|||||||
icon: Icons.person_outline,
|
icon: Icons.person_outline,
|
||||||
label: '书评人:',
|
label: '书评人:',
|
||||||
value: _review.reviewer.isNotEmpty ? _review.reviewer : '匿名',
|
value: _review.reviewer.isNotEmpty ? _review.reviewer : '匿名',
|
||||||
|
colors: colors,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -102,6 +104,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
|||||||
icon: Icons.source_outlined,
|
icon: Icons.source_outlined,
|
||||||
label: '来源:',
|
label: '来源:',
|
||||||
value: _review.source,
|
value: _review.source,
|
||||||
|
colors: colors,
|
||||||
),
|
),
|
||||||
|
|
||||||
if (_review.source.isNotEmpty) const SizedBox(height: 16),
|
if (_review.source.isNotEmpty) const SizedBox(height: 16),
|
||||||
@@ -111,6 +114,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
|||||||
icon: Icons.category_outlined,
|
icon: Icons.category_outlined,
|
||||||
label: '类型:',
|
label: '类型:',
|
||||||
value: _review.typeText,
|
value: _review.typeText,
|
||||||
|
colors: colors,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -120,6 +124,7 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
|||||||
icon: Icons.access_time,
|
icon: Icons.access_time,
|
||||||
label: '时间:',
|
label: '时间:',
|
||||||
value: _formatDate(_review.createdAt),
|
value: _formatDate(_review.createdAt),
|
||||||
|
colors: colors,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -132,29 +137,30 @@ class _BookReviewDetailPageState extends State<BookReviewDetailPage> {
|
|||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String label,
|
required String label,
|
||||||
required String value,
|
required String value,
|
||||||
|
required ColorScheme colors,
|
||||||
}) {
|
}) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: const Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
value,
|
value,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -47,10 +47,11 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final isEdit = widget.review != null;
|
final isEdit = widget.review != null;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(isEdit ? '编辑书评' : '写书评'),
|
title: Text(isEdit ? '编辑书评' : '写书评'),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -74,24 +75,24 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
|||||||
// 顶部信息栏
|
// 顶部信息栏
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
|
bottom: BorderSide(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// 类型选择
|
// 类型选择
|
||||||
_buildTypeSelector(),
|
_buildTypeSelector(colors),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
// 评论人
|
// 评论人
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _reviewerController,
|
controller: _reviewerController,
|
||||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '评论人',
|
hintText: '评论人',
|
||||||
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
|
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
@@ -104,10 +105,10 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
|||||||
width: 100,
|
width: 100,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _sourceController,
|
controller: _sourceController,
|
||||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '来源',
|
hintText: '来源',
|
||||||
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
|
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
@@ -125,19 +126,19 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
|||||||
maxLines: null,
|
maxLines: null,
|
||||||
expands: true,
|
expands: true,
|
||||||
textAlignVertical: TextAlignVertical.top,
|
textAlignVertical: TextAlignVertical.top,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.7,
|
height: 1.7,
|
||||||
),
|
),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '写下你的书评...',
|
hintText: '写下你的书评...',
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
),
|
),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.all(16),
|
contentPadding: const EdgeInsets.all(16),
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
@@ -154,29 +155,29 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 构建类型选择器
|
/// 构建类型选择器
|
||||||
Widget _buildTypeSelector() {
|
Widget _buildTypeSelector(ColorScheme colors) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => _showTypeSelector(),
|
onTap: () => _showTypeSelector(),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
border: Border.all(color: colors.outline),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_reviewType == 1 ? '短评' : '长评',
|
_reviewType == 1 ? '短评' : '长评',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.arrow_drop_down,
|
Icons.arrow_drop_down,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -188,36 +189,42 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
|||||||
void _showTypeSelector() {
|
void _showTypeSelector() {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.transparent,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
builder: (context) => SafeArea(
|
builder: (context) {
|
||||||
child: Column(
|
final colors = Theme.of(context).colorScheme;
|
||||||
mainAxisSize: MainAxisSize.min,
|
return Container(
|
||||||
children: [
|
color: colors.surface,
|
||||||
ListTile(
|
child: SafeArea(
|
||||||
title: const Text('短评'),
|
child: Column(
|
||||||
trailing: _reviewType == 1
|
mainAxisSize: MainAxisSize.min,
|
||||||
? const Icon(Icons.check, color: Color(0xFF1A1A1A))
|
children: [
|
||||||
: null,
|
ListTile(
|
||||||
onTap: () {
|
title: const Text('短评'),
|
||||||
setState(() => _reviewType = 1);
|
trailing: _reviewType == 1
|
||||||
Navigator.pop(context);
|
? Icon(Icons.check, color: colors.onSurface)
|
||||||
},
|
: null,
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _reviewType = 1);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Divider(height: 0.5, color: colors.outline),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('长评'),
|
||||||
|
trailing: _reviewType == 2
|
||||||
|
? Icon(Icons.check, color: colors.onSurface)
|
||||||
|
: null,
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _reviewType = 2);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5),
|
),
|
||||||
ListTile(
|
);
|
||||||
title: const Text('长评'),
|
},
|
||||||
trailing: _reviewType == 2
|
|
||||||
? const Icon(Icons.check, color: Color(0xFF1A1A1A))
|
|
||||||
: null,
|
|
||||||
onTap: () {
|
|
||||||
setState(() => _reviewType = 2);
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,4 +266,3 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,19 +73,20 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: _isSearching
|
title: _isSearching
|
||||||
? TextField(
|
? TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '搜索书评内容、书评人、来源...',
|
hintText: '搜索书评内容、书评人、来源...',
|
||||||
hintStyle: TextStyle(color: Color(0xFF999999)),
|
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
style: const TextStyle(color: Color(0xFF1A1A1A)),
|
style: TextStyle(color: colors.onSurface),
|
||||||
onChanged: _onSearchChanged,
|
onChanged: _onSearchChanged,
|
||||||
)
|
)
|
||||||
: const Text('书评'),
|
: const Text('书评'),
|
||||||
@@ -112,6 +113,7 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState() {
|
Widget _buildEmptyState() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -120,21 +122,21 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
|||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.rate_review_outlined,
|
Icons.rate_review_outlined,
|
||||||
size: 40,
|
size: 40,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Text(
|
Text(
|
||||||
'暂无书评',
|
'暂无书评',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -143,15 +145,15 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'添加记录',
|
'添加记录',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Colors.white,
|
color: colors.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -176,13 +178,14 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildReviewCard(BookReview review) {
|
Widget _buildReviewCard(BookReview review) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => _navigateToReviewDetail(review),
|
onTap: () => _navigateToReviewDetail(review),
|
||||||
onLongPress: () => _showDeleteDialog(review),
|
onLongPress: () => _showDeleteDialog(review),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -193,8 +196,8 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: review.reviewType == 1
|
color: review.reviewType == 1
|
||||||
? Colors.white
|
? colors.surface
|
||||||
: const Color(0xFF1A1A1A),
|
: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -202,8 +205,8 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: review.reviewType == 1
|
color: review.reviewType == 1
|
||||||
? const Color(0xFF666666)
|
? colors.onSurface.withValues(alpha: 0.6)
|
||||||
: Colors.white,
|
: colors.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -215,9 +218,9 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
|||||||
review.content,
|
review.content,
|
||||||
maxLines: review.reviewType == 1 ? 4 : 8,
|
maxLines: review.reviewType == 1 ? 4 : 8,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -232,9 +235,9 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
review.reviewer,
|
review.reviewer,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -252,9 +255,9 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
review.source,
|
review.source,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -262,9 +265,9 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
|||||||
// 日期
|
// 日期
|
||||||
Text(
|
Text(
|
||||||
_formatDate(review.createdAt),
|
_formatDate(review.createdAt),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -315,28 +318,31 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
|||||||
void _showDeleteDialog(BookReview review) {
|
void _showDeleteDialog(BookReview review) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) {
|
||||||
backgroundColor: Colors.white,
|
final colors = Theme.of(context).colorScheme;
|
||||||
elevation: 0,
|
return AlertDialog(
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
backgroundColor: colors.surface,
|
||||||
title: const Text('确认删除'),
|
elevation: 0,
|
||||||
content: const Text('确定要删除这条书评吗?'),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
actions: [
|
title: const Text('确认删除'),
|
||||||
TextButton(
|
content: const Text('确定要删除这条书评吗?'),
|
||||||
onPressed: () => Navigator.pop(context),
|
actions: [
|
||||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
TextButton(
|
||||||
),
|
onPressed: () => Navigator.pop(context),
|
||||||
TextButton(
|
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
onPressed: () async {
|
),
|
||||||
await context.read<AppProvider>().removeBookReview(review.id);
|
TextButton(
|
||||||
Navigator.pop(context);
|
onPressed: () async {
|
||||||
_loadReviews();
|
await context.read<AppProvider>().removeBookReview(review.id);
|
||||||
ToastUtil.show(context, '已删除');
|
Navigator.pop(context);
|
||||||
},
|
_loadReviews();
|
||||||
child: const Text('删除', style: TextStyle(color: Colors.red)),
|
ToastUtil.show(context, '已删除');
|
||||||
),
|
},
|
||||||
],
|
child: Text('删除', style: TextStyle(color: colors.error)),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,21 +23,22 @@ class _BookSharePageState extends State<BookSharePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: colors.surfaceContainerHighest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)),
|
icon: Icon(Icons.close, color: colors.onSurface),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'分享海报',
|
'分享海报',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
@@ -50,12 +51,12 @@ class _BookSharePageState extends State<BookSharePage> {
|
|||||||
height: 20,
|
height: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Text(
|
: Text(
|
||||||
'分享',
|
'分享',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -76,13 +77,14 @@ class _BookSharePageState extends State<BookSharePage> {
|
|||||||
|
|
||||||
/// 构建海报 Widget
|
/// 构建海报 Widget
|
||||||
Widget _buildPosterWidget() {
|
Widget _buildPosterWidget() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final book = widget.book;
|
final book = widget.book;
|
||||||
final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty;
|
final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 320,
|
width: 320,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -117,10 +119,10 @@ class _BookSharePageState extends State<BookSharePage> {
|
|||||||
// 书名
|
// 书名
|
||||||
Text(
|
Text(
|
||||||
book.title,
|
book.title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -129,9 +131,9 @@ class _BookSharePageState extends State<BookSharePage> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
book.alternateTitles.join(' / '),
|
book.alternateTitles.join(' / '),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -157,11 +159,11 @@ class _BookSharePageState extends State<BookSharePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
const Text(
|
Text(
|
||||||
'/ 10',
|
'/ 10',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -171,44 +173,45 @@ class _BookSharePageState extends State<BookSharePage> {
|
|||||||
|
|
||||||
// 作者
|
// 作者
|
||||||
if (book.authors.isNotEmpty)
|
if (book.authors.isNotEmpty)
|
||||||
_buildInfoRow('作者', book.authors.join(' / ')),
|
_buildInfoRow('作者', book.authors.join(' / '), colors),
|
||||||
|
|
||||||
// 出版社
|
// 出版社
|
||||||
if (book.publisher != null && book.publisher!.isNotEmpty)
|
if (book.publisher != null && book.publisher!.isNotEmpty)
|
||||||
_buildInfoRow('出版社', book.publisher!),
|
_buildInfoRow('出版社', book.publisher!, colors),
|
||||||
|
|
||||||
// 出版时间
|
// 出版时间
|
||||||
if (book.publishDate != null)
|
if (book.publishDate != null)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
'出版',
|
'出版',
|
||||||
'${book.publishDate!.year}.${book.publishDate!.month.toString().padLeft(2, '0')}.${book.publishDate!.day.toString().padLeft(2, '0')}',
|
'${book.publishDate!.year}.${book.publishDate!.month.toString().padLeft(2, '0')}.${book.publishDate!.day.toString().padLeft(2, '0')}',
|
||||||
|
colors,
|
||||||
),
|
),
|
||||||
|
|
||||||
// 类型
|
// 类型
|
||||||
if (book.genres.isNotEmpty)
|
if (book.genres.isNotEmpty)
|
||||||
_buildInfoRow('类型', book.genres.join(' / ')),
|
_buildInfoRow('类型', book.genres.join(' / '), colors),
|
||||||
|
|
||||||
// ISBN
|
// ISBN
|
||||||
if (book.isbn != null && book.isbn!.isNotEmpty)
|
if (book.isbn != null && book.isbn!.isNotEmpty)
|
||||||
_buildInfoRow('ISBN', book.isbn!),
|
_buildInfoRow('ISBN', book.isbn!, colors),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 简介
|
// 简介
|
||||||
if (book.summary != null && book.summary!.isNotEmpty) ...[
|
if (book.summary != null && book.summary!.isNotEmpty) ...[
|
||||||
const Text(
|
Text(
|
||||||
'简介',
|
'简介',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
book.summary!,
|
book.summary!,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
height: 1.6,
|
height: 1.6,
|
||||||
),
|
),
|
||||||
maxLines: 5,
|
maxLines: 5,
|
||||||
@@ -219,7 +222,7 @@ class _BookSharePageState extends State<BookSharePage> {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// 底部标识
|
// 底部标识
|
||||||
const Divider(height: 1, color: Color(0xFFE8E8E8)),
|
Divider(height: 1, color: colors.outline),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -227,14 +230,14 @@ class _BookSharePageState extends State<BookSharePage> {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.book_outlined,
|
Icons.book_outlined,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: const Color(0xFF1A1A1A).withOpacity(0.5),
|
color: colors.onSurface.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'来自 MookNote',
|
'来自 MookNote',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: const Color(0xFF1A1A1A).withOpacity(0.5),
|
color: colors.onSurface.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -248,7 +251,7 @@ class _BookSharePageState extends State<BookSharePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 构建信息行
|
/// 构建信息行
|
||||||
Widget _buildInfoRow(String label, String value) {
|
Widget _buildInfoRow(String label, String value, ColorScheme colors) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -256,17 +259,17 @@ class _BookSharePageState extends State<BookSharePage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'$label:',
|
'$label:',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
value,
|
value,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF333333),
|
color: colors.onSurface.withValues(alpha: 0.75),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class BookTabPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _BookTabPageState extends State<BookTabPage> {
|
class _BookTabPageState extends State<BookTabPage> {
|
||||||
int _layoutStyle = 0; // 0: 封面网格, 1: 列表
|
int _layoutStyle = 0;
|
||||||
bool _firstLoad = true;
|
bool _firstLoad = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -42,6 +42,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBookList(BuildContext context) {
|
Widget _buildBookList(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
final statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'};
|
final statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'};
|
||||||
@@ -55,8 +56,8 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
if (books.isEmpty) {
|
if (books.isEmpty) {
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async => await provider.loadBooks(),
|
onRefresh: () async => await provider.loadBooks(),
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
children: [_buildEmptyState(context, provider.bookStatusIndex)],
|
children: [_buildEmptyState(context, provider.bookStatusIndex)],
|
||||||
@@ -73,10 +74,11 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildGridView(List books, AppProvider provider) {
|
Widget _buildGridView(List books, AppProvider provider) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async => await provider.loadBooks(),
|
onRefresh: () async => await provider.loadBooks(),
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 100),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 100),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
@@ -92,10 +94,11 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildListView(List books, AppProvider provider) {
|
Widget _buildListView(List books, AppProvider provider) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async => await provider.loadBooks(),
|
onRefresh: () async => await provider.loadBooks(),
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
||||||
itemCount: books.length,
|
itemCount: books.length,
|
||||||
@@ -105,6 +108,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildListCard(book) {
|
Widget _buildListCard(book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => Navigator.pushNamed(context, '/book-detail', arguments: book),
|
onTap: () => Navigator.pushNamed(context, '/book-detail', arguments: book),
|
||||||
onLongPress: () => _showDeleteDialog(context, book),
|
onLongPress: () => _showDeleteDialog(context, book),
|
||||||
@@ -112,23 +116,22 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// 封面缩略图
|
|
||||||
Container(
|
Container(
|
||||||
width: 48, height: 64,
|
width: 48, height: 64,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF0F0F0),
|
color: colors.outlineVariant,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: book.coverPath != null && book.coverPath!.isNotEmpty
|
child: book.coverPath != null && book.coverPath!.isNotEmpty
|
||||||
? Image.file(File(book.coverPath!), fit: BoxFit.cover,
|
? Image.file(File(book.coverPath!), fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) => const Icon(Icons.menu_book_outlined, size: 22, color: Color(0xFFCCCCCC)))
|
errorBuilder: (_, __, ___) => Icon(Icons.menu_book_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)))
|
||||||
: const Icon(Icons.menu_book_outlined, size: 22, color: Color(0xFFCCCCCC)),
|
: Icon(Icons.menu_book_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -136,11 +139,11 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(book.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
Text(book.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
if (book.authors.isNotEmpty) ...[
|
if (book.authors.isNotEmpty) ...[
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text(book.authors.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis,
|
Text(book.authors.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
if (book.rating != null)
|
if (book.rating != null)
|
||||||
@@ -151,7 +154,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
|
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -159,19 +162,20 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showDeleteDialog(BuildContext context, book) {
|
void _showDeleteDialog(BuildContext context, book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: const Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
content: Text('确定要删除《${book.title}》吗?删除后可在回收站恢复。',
|
content: Text('确定要删除《${book.title}》吗?删除后可在回收站恢复。',
|
||||||
style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)),
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx),
|
onPressed: () => Navigator.pop(ctx),
|
||||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -179,7 +183,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red, foregroundColor: Colors.white, elevation: 0,
|
backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
@@ -193,11 +197,12 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
|
|
||||||
Widget _buildSkeleton() {
|
Widget _buildSkeleton() {
|
||||||
return _layoutStyle == 1
|
return _layoutStyle == 1
|
||||||
? MovieSkeletonGrid() // reuse same grid skeleton pattern
|
? MovieSkeletonGrid()
|
||||||
: const BookSkeletonGrid();
|
: const BookSkeletonGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState(BuildContext context, int statusIndex) {
|
Widget _buildEmptyState(BuildContext context, int statusIndex) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final statusText = ['已读', '在读', '想读'][statusIndex];
|
final statusText = ['已读', '在读', '想读'][statusIndex];
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -205,11 +210,11 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 80, height: 80,
|
width: 80, height: 80,
|
||||||
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(20)),
|
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)),
|
||||||
child: const Icon(Icons.menu_book_outlined, size: 40, color: Color(0xFFCCCCCC)),
|
child: Icon(Icons.menu_book_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text('暂无$statusText的书籍', style: const TextStyle(fontSize: 16, color: Color(0xFF999999))),
|
Text('暂无$statusText的书籍', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -218,8 +223,8 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(8)),
|
decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(8)),
|
||||||
child: const Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
child: Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class _HomePageState extends State<HomePage> {
|
|||||||
width: 44,
|
width: 44,
|
||||||
height: 56,
|
height: 56,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
borderRadius: const BorderRadius.horizontal(
|
borderRadius: const BorderRadius.horizontal(
|
||||||
right: Radius.circular(28)),
|
right: Radius.circular(28)),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
@@ -111,10 +111,10 @@ class _HomePageState extends State<HomePage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.chevron_right,
|
Icons.chevron_right,
|
||||||
color: Color(0xFF999999),
|
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4),
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
_loadTabSettings();
|
_loadTabSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 加载标签显示设置
|
|
||||||
void _loadTabSettings() {
|
void _loadTabSettings() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_showMovieTab = _userPrefs.showMovieTab;
|
_showMovieTab = _userPrefs.showMovieTab;
|
||||||
@@ -43,11 +42,9 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
// 当页面重新获得焦点时刷新设置
|
|
||||||
_loadTabSettings();
|
_loadTabSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取启用的标签列表
|
|
||||||
List<_TabItem> get _enabledTabs {
|
List<_TabItem> get _enabledTabs {
|
||||||
final tabs = <_TabItem>[];
|
final tabs = <_TabItem>[];
|
||||||
if (_showMovieTab) tabs.add(_TabItem('影视', 0));
|
if (_showMovieTab) tabs.add(_TabItem('影视', 0));
|
||||||
@@ -56,7 +53,6 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
return tabs;
|
return tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将原始索引映射到启用标签的索引
|
|
||||||
int _mapToEnabledTabIndex(int originalIndex) {
|
int _mapToEnabledTabIndex(int originalIndex) {
|
||||||
final tabs = _enabledTabs;
|
final tabs = _enabledTabs;
|
||||||
for (int i = 0; i < tabs.length; i++) {
|
for (int i = 0; i < tabs.length; i++) {
|
||||||
@@ -69,13 +65,8 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// 顶部 AppBar
|
|
||||||
_buildAppBar(context),
|
_buildAppBar(context),
|
||||||
|
|
||||||
// 三个标签页的标题栏
|
|
||||||
_buildTabBar(context),
|
_buildTabBar(context),
|
||||||
|
|
||||||
// 标签页内容
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildTabContent(),
|
child: _buildTabContent(),
|
||||||
),
|
),
|
||||||
@@ -83,16 +74,13 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建顶部 AppBar
|
|
||||||
Widget _buildAppBar(BuildContext context) {
|
Widget _buildAppBar(BuildContext context) {
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
title: Text(_getAppBarTitle(provider)),
|
title: Text(_getAppBarTitle(provider)),
|
||||||
actions: [
|
actions: [
|
||||||
// 云备份按钮
|
|
||||||
_buildCloudSyncButton(context),
|
_buildCloudSyncButton(context),
|
||||||
// 搜索按钮
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.search),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -110,8 +98,8 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建云备份按钮
|
|
||||||
Widget _buildCloudSyncButton(BuildContext context) {
|
Widget _buildCloudSyncButton(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return PopupMenuButton<String>(
|
return PopupMenuButton<String>(
|
||||||
icon: const Icon(Icons.cloud_sync_outlined),
|
icon: const Icon(Icons.cloud_sync_outlined),
|
||||||
tooltip: '云备份',
|
tooltip: '云备份',
|
||||||
@@ -128,21 +116,21 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.cloud_upload_outlined,
|
Icons.cloud_upload_outlined,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
const Text(
|
Text(
|
||||||
'上传数据',
|
'上传数据',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -156,21 +144,21 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.cloud_download_outlined,
|
Icons.cloud_download_outlined,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
const Text(
|
Text(
|
||||||
'下载数据',
|
'下载数据',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -185,21 +173,21 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.settings_outlined,
|
Icons.settings_outlined,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
const Text(
|
Text(
|
||||||
'WebDAV设置',
|
'WebDAV设置',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -227,9 +215,8 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 执行同步操作
|
|
||||||
Future<void> _performSync(BuildContext context, SyncDirection direction) async {
|
Future<void> _performSync(BuildContext context, SyncDirection direction) async {
|
||||||
// 检查是否已配置
|
final colors = Theme.of(context).colorScheme;
|
||||||
final config = await WebDAVService.instance.getConfig();
|
final config = await WebDAVService.instance.getConfig();
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@@ -243,28 +230,24 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示加载对话框
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) => const Center(
|
builder: (context) => Center(
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行同步
|
|
||||||
final result = await WebDAVService.instance.syncData(direction: direction);
|
final result = await WebDAVService.instance.syncData(direction: direction);
|
||||||
|
|
||||||
// 关闭加载对话框
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载了数据则刷新界面
|
|
||||||
if (result.success && result.needReload && context.mounted) {
|
if (result.success && result.needReload && context.mounted) {
|
||||||
final provider = context.read<AppProvider>();
|
final provider = context.read<AppProvider>();
|
||||||
await provider.loadMovies();
|
await provider.loadMovies();
|
||||||
@@ -272,11 +255,10 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
await provider.loadNotes();
|
await provider.loadNotes();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示结果
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
final isSuccess = result.success;
|
final isSuccess = result.success;
|
||||||
final message = result.message.isNotEmpty ? result.message : (isSuccess ? '同步成功' : '同步失败');
|
final message = result.message.isNotEmpty ? result.message : (isSuccess ? '同步成功' : '同步失败');
|
||||||
|
|
||||||
_showResultDialog(
|
_showResultDialog(
|
||||||
context,
|
context,
|
||||||
title: isSuccess ? '同步成功' : '同步失败',
|
title: isSuccess ? '同步成功' : '同步失败',
|
||||||
@@ -290,7 +272,6 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示同步结果对话框
|
|
||||||
void _showResultDialog(
|
void _showResultDialog(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required String title,
|
required String title,
|
||||||
@@ -298,10 +279,11 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
required bool isSuccess,
|
required bool isSuccess,
|
||||||
Map<String, dynamic>? details,
|
Map<String, dynamic>? details,
|
||||||
}) {
|
}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: Row(
|
title: Row(
|
||||||
@@ -322,9 +304,10 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -335,9 +318,9 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
message,
|
message,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -346,7 +329,7 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -370,8 +353,8 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF1A1A1A),
|
backgroundColor: colors.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colors.onPrimary,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -386,8 +369,8 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建详情行
|
|
||||||
Widget _buildDetailRow(String label, String value, {bool isError = false}) {
|
Widget _buildDetailRow(String label, String value, {bool isError = false}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -395,9 +378,9 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -405,7 +388,7 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: isError ? const Color(0xFFE57373) : const Color(0xFF1A1A1A),
|
color: isError ? const Color(0xFFE57373) : colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -413,7 +396,6 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取 AppBar 标题
|
|
||||||
String _getAppBarTitle(AppProvider provider) {
|
String _getAppBarTitle(AppProvider provider) {
|
||||||
switch (provider.mainTabIndex) {
|
switch (provider.mainTabIndex) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -440,17 +422,17 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建标签栏
|
|
||||||
Widget _buildTabBar(BuildContext context) {
|
Widget _buildTabBar(BuildContext context) {
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final tabs = _enabledTabs;
|
final tabs = _enabledTabs;
|
||||||
final currentEnabledIndex = _mapToEnabledTabIndex(provider.mainTabIndex);
|
final currentEnabledIndex = _mapToEnabledTabIndex(provider.mainTabIndex);
|
||||||
final safeIndex = currentEnabledIndex < tabs.length ? currentEnabledIndex : 0;
|
final safeIndex = currentEnabledIndex < tabs.length ? currentEnabledIndex : 0;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -474,7 +456,7 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
Icon(
|
Icon(
|
||||||
_getTabIcon(tab.label),
|
_getTabIcon(tab.label),
|
||||||
size: 22,
|
size: 22,
|
||||||
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFB0B0B0),
|
color: isSelected ? colors.primary : colors.onSurface.withValues(alpha: 0.35),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
@@ -482,7 +464,7 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
||||||
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFB0B0B0),
|
color: isSelected ? colors.primary : colors.onSurface.withValues(alpha: 0.35),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -513,7 +495,7 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
width: indicatorWidth,
|
width: indicatorWidth,
|
||||||
height: 3,
|
height: 3,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(1.5),
|
borderRadius: BorderRadius.circular(1.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -524,7 +506,7 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -532,12 +514,10 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建标签页内容
|
|
||||||
Widget _buildTabContent() {
|
Widget _buildTabContent() {
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
final tabs = _enabledTabs;
|
final tabs = _enabledTabs;
|
||||||
// 找到当前应该显示的标签
|
|
||||||
_TabItem? currentTab;
|
_TabItem? currentTab;
|
||||||
for (final tab in tabs) {
|
for (final tab in tabs) {
|
||||||
if (tab.originalIndex == provider.mainTabIndex) {
|
if (tab.originalIndex == provider.mainTabIndex) {
|
||||||
@@ -545,10 +525,8 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果当前标签被禁用了,显示第一个启用的标签
|
|
||||||
if (currentTab == null && tabs.isNotEmpty) {
|
if (currentTab == null && tabs.isNotEmpty) {
|
||||||
currentTab = tabs.first;
|
currentTab = tabs.first;
|
||||||
// 更新 provider 的索引
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
provider.setMainTabIndex(currentTab!.originalIndex);
|
provider.setMainTabIndex(currentTab!.originalIndex);
|
||||||
});
|
});
|
||||||
@@ -572,22 +550,21 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示添加对话框
|
|
||||||
void _showAddDialog(BuildContext context, AppProvider provider) {
|
void _showAddDialog(BuildContext context, AppProvider provider) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.movie, color: Color(0xFF1A1A1A)),
|
leading: Icon(Icons.movie, color: colors.onSurface),
|
||||||
title: const Text('添加观影'),
|
title: const Text('添加观影'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
// 根据当前影视标签页的选中状态设置默认值
|
|
||||||
final statusMap = {
|
final statusMap = {
|
||||||
0: 'watched',
|
0: 'watched',
|
||||||
1: 'watching',
|
1: 'watching',
|
||||||
@@ -601,13 +578,12 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5, indent: 56),
|
Divider(height: 0.5, indent: 56, color: colors.outlineVariant),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.menu_book, color: Color(0xFF1A1A1A)),
|
leading: Icon(Icons.menu_book, color: colors.onSurface),
|
||||||
title: const Text('添加阅读'),
|
title: const Text('添加阅读'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
// 根据当前阅读标签页的选中状态设置默认值
|
|
||||||
final statusMap = {
|
final statusMap = {
|
||||||
0: 'read',
|
0: 'read',
|
||||||
1: 'reading',
|
1: 'reading',
|
||||||
@@ -621,9 +597,9 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5, indent: 56),
|
Divider(height: 0.5, indent: 56, color: colors.outlineVariant),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.note, color: Color(0xFF1A1A1A)),
|
leading: Icon(Icons.note, color: colors.onSurface),
|
||||||
title: const Text('添加笔记'),
|
title: const Text('添加笔记'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -638,7 +614,6 @@ class _MainContentPageState extends State<MainContentPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 标签项数据类
|
|
||||||
class _TabItem {
|
class _TabItem {
|
||||||
final String label;
|
final String label;
|
||||||
final int originalIndex;
|
final int originalIndex;
|
||||||
|
|||||||
@@ -90,26 +90,30 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
void _showPermissionDeniedDialog() {
|
void _showPermissionDeniedDialog() {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) {
|
||||||
title: const Text('需要存储权限', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
final colors = Theme.of(ctx).colorScheme;
|
||||||
content: const Text(
|
return AlertDialog(
|
||||||
'Android 11+ 需要在系统设置中授予"所有文件访问权限"才能读取目录中的 Markdown 文件。\n\n是否前往设置?',
|
title: const Text('需要存储权限',
|
||||||
style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||||
),
|
content: Text(
|
||||||
actions: [
|
'Android 11+ 需要在系统设置中授予"所有文件访问权限"才能读取目录中的 Markdown 文件。\n\n是否前往设置?',
|
||||||
TextButton(
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6),
|
||||||
onPressed: () => Navigator.pop(ctx),
|
|
||||||
child: const Text('取消', style: TextStyle(color: Color(0xFF999999))),
|
|
||||||
),
|
),
|
||||||
TextButton(
|
actions: [
|
||||||
onPressed: () {
|
TextButton(
|
||||||
Navigator.pop(ctx);
|
onPressed: () => Navigator.pop(ctx),
|
||||||
openAppSettings();
|
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
},
|
),
|
||||||
child: const Text('前往设置', style: TextStyle(color: Color(0xFF1A1A1A))),
|
TextButton(
|
||||||
),
|
onPressed: () {
|
||||||
],
|
Navigator.pop(ctx);
|
||||||
),
|
openAppSettings();
|
||||||
|
},
|
||||||
|
child: Text('前往设置', style: TextStyle(color: colors.onSurface)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +125,10 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
for (final entity in dir.listSync(recursive: true, followLinks: false)) {
|
for (final entity in dir.listSync(recursive: true, followLinks: false)) {
|
||||||
if (entity is File) {
|
if (entity is File) {
|
||||||
final lower = p.basename(entity.path).toLowerCase();
|
final lower = p.basename(entity.path).toLowerCase();
|
||||||
if (lower.endsWith('.md') || lower.endsWith('.markdown') || lower.endsWith('.mdown') || lower.endsWith('.txt')) {
|
if (lower.endsWith('.md') ||
|
||||||
|
lower.endsWith('.markdown') ||
|
||||||
|
lower.endsWith('.mdown') ||
|
||||||
|
lower.endsWith('.txt')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,13 +142,28 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
try {
|
try {
|
||||||
final dir = Directory(dirPath);
|
final dir = Directory(dirPath);
|
||||||
if (!dir.existsSync()) return false;
|
if (!dir.existsSync()) return false;
|
||||||
const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico', '.tiff', '.heic', '.webm'];
|
const imageExts = [
|
||||||
|
'.png',
|
||||||
|
'.jpg',
|
||||||
|
'.jpeg',
|
||||||
|
'.gif',
|
||||||
|
'.webp',
|
||||||
|
'.bmp',
|
||||||
|
'.svg',
|
||||||
|
'.ico',
|
||||||
|
'.tiff',
|
||||||
|
'.heic',
|
||||||
|
'.webm'
|
||||||
|
];
|
||||||
bool hasAnyFile = false;
|
bool hasAnyFile = false;
|
||||||
for (final entity in dir.listSync(recursive: true, followLinks: false)) {
|
for (final entity in dir.listSync(recursive: true, followLinks: false)) {
|
||||||
if (entity is File) {
|
if (entity is File) {
|
||||||
hasAnyFile = true;
|
hasAnyFile = true;
|
||||||
final lower = p.basename(entity.path).toLowerCase();
|
final lower = p.basename(entity.path).toLowerCase();
|
||||||
if (lower.endsWith('.md') || lower.endsWith('.markdown') || lower.endsWith('.mdown') || lower.endsWith('.txt')) {
|
if (lower.endsWith('.md') ||
|
||||||
|
lower.endsWith('.markdown') ||
|
||||||
|
lower.endsWith('.mdown') ||
|
||||||
|
lower.endsWith('.txt')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!imageExts.any((ext) => lower.endsWith(ext))) {
|
if (!imageExts.any((ext) => lower.endsWith(ext))) {
|
||||||
@@ -193,9 +215,13 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
}
|
}
|
||||||
} else if (entity is File) {
|
} else if (entity is File) {
|
||||||
final lower = name.toLowerCase();
|
final lower = name.toLowerCase();
|
||||||
if (lower.endsWith('.md') || lower.endsWith('.markdown') || lower.endsWith('.mdown') || lower.endsWith('.txt')) {
|
if (lower.endsWith('.md') ||
|
||||||
|
lower.endsWith('.markdown') ||
|
||||||
|
lower.endsWith('.mdown') ||
|
||||||
|
lower.endsWith('.txt')) {
|
||||||
final stat = entity.statSync();
|
final stat = entity.statSync();
|
||||||
entries.add(_FileEntry(name: name, path: entity.path, isDir: false, size: stat.size));
|
entries.add(_FileEntry(
|
||||||
|
name: name, path: entity.path, isDir: false, size: stat.size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,9 +262,10 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _openFile(_FileEntry entry) {
|
void _openFile(_FileEntry entry) {
|
||||||
Navigator.push(context, MaterialPageRoute(
|
Navigator.push(context,
|
||||||
builder: (context) => MdViewerPage(filePath: entry.path),
|
MaterialPageRoute(
|
||||||
));
|
builder: (context) => MdViewerPage(filePath: entry.path),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showSettingsSheet() {
|
void _showSettingsSheet() {
|
||||||
@@ -249,6 +276,7 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
),
|
),
|
||||||
builder: (ctx) {
|
builder: (ctx) {
|
||||||
|
final colors = Theme.of(ctx).colorScheme;
|
||||||
return StatefulBuilder(
|
return StatefulBuilder(
|
||||||
builder: (ctx, setLocalState) => Padding(
|
builder: (ctx, setLocalState) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
@@ -256,24 +284,31 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 36, height: 4,
|
width: 36,
|
||||||
|
height: 4,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFDDDDDD),
|
color: colors.onSurface.withValues(alpha: 0.15),
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text('目录显示设置', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
child: Text('目录显示设置',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: const Text('显示空目录', style: TextStyle(fontSize: 14, color: Color(0xFF333333))),
|
title: Text('显示空目录',
|
||||||
subtitle: const Text('关闭后隐藏无 Markdown 文件的目录', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
style: TextStyle(
|
||||||
|
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75))),
|
||||||
|
subtitle: Text('关闭后隐藏无 Markdown 文件的目录',
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
value: _showEmptyDirs,
|
value: _showEmptyDirs,
|
||||||
activeColor: const Color(0xFF1A1A1A),
|
activeTrackColor: colors.primary,
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
setLocalState(() => _showEmptyDirs = val);
|
setLocalState(() => _showEmptyDirs = val);
|
||||||
UserPrefs().setShowEmptyDirs(val);
|
UserPrefs().setShowEmptyDirs(val);
|
||||||
@@ -281,13 +316,17 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
_loadDirectory();
|
_loadDirectory();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5, color: Color(0xFFF0F0F0)),
|
Divider(height: 0.5, color: colors.outlineVariant),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: const Text('显示纯图片目录', style: TextStyle(fontSize: 14, color: Color(0xFF333333))),
|
title: Text('显示纯图片目录',
|
||||||
subtitle: const Text('关闭后隐藏只含图片、无 Markdown 的目录', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
style: TextStyle(
|
||||||
|
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75))),
|
||||||
|
subtitle: Text('关闭后隐藏只含图片、无 Markdown 的目录',
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
value: _showImageOnlyDirs,
|
value: _showImageOnlyDirs,
|
||||||
activeColor: const Color(0xFF1A1A1A),
|
activeTrackColor: colors.primary,
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
setLocalState(() => _showImageOnlyDirs = val);
|
setLocalState(() => _showImageOnlyDirs = val);
|
||||||
UserPrefs().setShowImageOnlyDirs(val);
|
UserPrefs().setShowImageOnlyDirs(val);
|
||||||
@@ -304,40 +343,26 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _canGoBack => _currentPath != null && _rootPath != null && _currentPath != _rootPath;
|
bool get _canGoBack =>
|
||||||
|
_currentPath != null && _rootPath != null && _currentPath != _rootPath;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: Text(
|
title: Text(
|
||||||
_currentPath != null ? p.basename(_currentPath!) : 'Markdown 阅读',
|
_currentPath != null ? p.basename(_currentPath!) : 'Markdown 阅读',
|
||||||
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||||
),
|
),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Color(0xFF1A1A1A)),
|
icon: Icon(Icons.arrow_back, color: colors.onSurface),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
// if (_canGoBack)
|
|
||||||
// Padding(
|
|
||||||
// padding: const EdgeInsets.only(right: 8),
|
|
||||||
// child: GestureDetector(
|
|
||||||
// onTap: _goBack,
|
|
||||||
// child: Container(
|
|
||||||
// padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
|
||||||
// decoration: BoxDecoration(
|
|
||||||
// color: const Color(0xFFF5F5F5),
|
|
||||||
// borderRadius: BorderRadius.circular(14),
|
|
||||||
// border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
|
||||||
// ),
|
|
||||||
// child: const Text('返回上级', style: TextStyle(fontSize: 12, color: Color(0xFF666666))),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
if (_currentPath != null)
|
if (_currentPath != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 4),
|
padding: const EdgeInsets.only(right: 4),
|
||||||
@@ -346,86 +371,109 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
child: const Text('更换目录', style: TextStyle(fontSize: 12, color: Color(0xFF888888))),
|
child: Text('更换目录',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.tune, size: 20, color: Color(0xFF888888)),
|
icon: Icon(Icons.tune, size: 20, color: colors.onSurface.withValues(alpha: 0.5)),
|
||||||
onPressed: _showSettingsSheet,
|
onPressed: _showSettingsSheet,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: _buildBody(),
|
body: _buildBody(colors),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody(ColorScheme colors) {
|
||||||
if (_currentPath == null) {
|
if (_currentPath == null) {
|
||||||
return _buildWelcome();
|
return _buildWelcome(colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator(color: Color(0xFF1A1A1A)));
|
return Center(child: CircularProgressIndicator(color: colors.primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_error != null) {
|
if (_error != null) {
|
||||||
return _buildError();
|
return _buildError(colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: _loadDirectory,
|
onRefresh: _loadDirectory,
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
child: _entries.isEmpty ? _buildEmpty() : ListView.separated(
|
child: _entries.isEmpty
|
||||||
padding: EdgeInsets.zero,
|
? _buildEmpty(colors)
|
||||||
itemCount: (_canGoBack ? 1 : 0) + _entries.length,
|
: ListView.separated(
|
||||||
separatorBuilder: (_, __) => const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFF0F0F0)),
|
padding: EdgeInsets.zero,
|
||||||
itemBuilder: (context, index) {
|
itemCount: (_canGoBack ? 1 : 0) + _entries.length,
|
||||||
if (_canGoBack && index == 0) {
|
separatorBuilder: (_, __) =>
|
||||||
return ListTile(
|
Divider(height: 0.5, thickness: 0.5, color: colors.outlineVariant),
|
||||||
leading: Container(
|
itemBuilder: (context, index) {
|
||||||
width: 36, height: 36,
|
if (_canGoBack && index == 0) {
|
||||||
decoration: BoxDecoration(
|
return ListTile(
|
||||||
color: const Color(0xFFF5F5F5),
|
leading: Container(
|
||||||
borderRadius: BorderRadius.circular(8),
|
width: 36,
|
||||||
),
|
height: 36,
|
||||||
child: const Icon(Icons.arrow_upward, size: 18, color: Color(0xFF888888)),
|
decoration: BoxDecoration(
|
||||||
),
|
color: colors.surfaceContainerHighest,
|
||||||
title: const Text('..', style: TextStyle(fontSize: 14, color: Color(0xFF888888))),
|
borderRadius: BorderRadius.circular(8),
|
||||||
onTap: _goBack,
|
),
|
||||||
);
|
child: Icon(Icons.arrow_upward,
|
||||||
}
|
size: 18, color: colors.onSurface.withValues(alpha: 0.5)),
|
||||||
final entry = _entries[_canGoBack ? index - 1 : index];
|
),
|
||||||
return ListTile(
|
title: Text('..',
|
||||||
leading: Container(
|
style: TextStyle(
|
||||||
width: 36, height: 36,
|
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||||
decoration: BoxDecoration(
|
onTap: _goBack,
|
||||||
color: entry.isDir ? const Color(0xFFF0F7FF) : const Color(0xFFF5F5F5),
|
);
|
||||||
borderRadius: BorderRadius.circular(8),
|
}
|
||||||
),
|
final entry = _entries[_canGoBack ? index - 1 : index];
|
||||||
child: Icon(
|
return ListTile(
|
||||||
entry.isDir ? Icons.folder_outlined : Icons.description_outlined,
|
leading: Container(
|
||||||
size: 18,
|
width: 36,
|
||||||
color: entry.isDir ? const Color(0xFF4A90D9) : const Color(0xFF666666),
|
height: 36,
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
|
color: entry.isDir
|
||||||
|
? const Color(0xFFF0F7FF)
|
||||||
|
: colors.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
entry.isDir ? Icons.folder_outlined : Icons.description_outlined,
|
||||||
|
size: 18,
|
||||||
|
color: entry.isDir
|
||||||
|
? const Color(0xFF4A90D9)
|
||||||
|
: colors.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(entry.name,
|
||||||
|
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis),
|
||||||
|
subtitle: entry.isDir
|
||||||
|
? null
|
||||||
|
: Text(_formatSize(entry.size),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
|
trailing: Icon(entry.isDir ? Icons.chevron_right : Icons.open_in_new_outlined,
|
||||||
|
size: 16, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
|
onTap: () =>
|
||||||
|
entry.isDir ? _enterDirectory(entry.path) : _openFile(entry),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
title: Text(entry.name, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)), maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
||||||
subtitle: entry.isDir ? null : Text(_formatSize(entry.size), style: const TextStyle(fontSize: 11, color: Color(0xFF999999))),
|
|
||||||
trailing: Icon(entry.isDir ? Icons.chevron_right : Icons.open_in_new_outlined, size: 16, color: const Color(0xFFCCCCCC)),
|
|
||||||
onTap: () => entry.isDir ? _enterDirectory(entry.path) : _openFile(entry),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildWelcome() {
|
Widget _buildWelcome(ColorScheme colors) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(40),
|
padding: const EdgeInsets.all(40),
|
||||||
@@ -433,27 +481,34 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 80, height: 80,
|
width: 80,
|
||||||
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.folder_open_outlined, size: 40, color: Color(0xFFCCCCCC)),
|
child: Icon(Icons.folder_open_outlined,
|
||||||
|
size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const Text('Markdown 阅读', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
Text('Markdown 阅读',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text('选择一个包含 .md 文件的文件夹', style: TextStyle(fontSize: 14, color: Color(0xFF999999))),
|
Text('选择一个包含 .md 文件的文件夹',
|
||||||
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _pickDirectory,
|
onTap: _pickDirectory,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14),
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
),
|
),
|
||||||
child: const Text('选择目录', style: TextStyle(fontSize: 15, color: Colors.white, fontWeight: FontWeight.w500)),
|
child: Text('选择目录',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15, color: colors.onPrimary, fontWeight: FontWeight.w500)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -462,28 +517,31 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmpty() {
|
Widget _buildEmpty(ColorScheme colors) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.folder_open_outlined, size: 64, color: Color(0xFFE0E0E0)),
|
Icon(Icons.folder_open_outlined, size: 64, color: colors.outline),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text('此目录下没有 Markdown 文件', style: TextStyle(fontSize: 15, color: Color(0xFF999999))),
|
Text('此目录下没有 Markdown 文件',
|
||||||
|
style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(_currentPath ?? '', style: const TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))),
|
Text(_currentPath ?? '',
|
||||||
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _canGoBack ? _goBack : () => _pickDirectory(),
|
onTap: _canGoBack ? _goBack : () => _pickDirectory(),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: const Color(0xFFDDDDDD)),
|
border: Border.all(color: colors.onSurface.withValues(alpha: 0.15)),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
_canGoBack ? '返回上级目录' : '换一个目录',
|
_canGoBack ? '返回上级目录' : '换一个目录',
|
||||||
style: const TextStyle(fontSize: 13, color: Color(0xFF888888)),
|
style: TextStyle(
|
||||||
|
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -492,18 +550,22 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildError() {
|
Widget _buildError(ColorScheme colors) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(40),
|
padding: const EdgeInsets.all(40),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.error_outline, size: 48, color: Color(0xFFCCCCCC)),
|
Icon(Icons.error_outline,
|
||||||
|
size: 48, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(_error!, style: const TextStyle(fontSize: 14, color: Color(0xFF999999)), textAlign: TextAlign.center),
|
Text(_error!,
|
||||||
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
|
textAlign: TextAlign.center),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text('路径: ${_currentPath ?? ""}', style: const TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))),
|
Text('路径: ${_currentPath ?? ""}',
|
||||||
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -513,10 +575,11 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Text('重试', style: TextStyle(fontSize: 13, color: Colors.white)),
|
child: Text('重试',
|
||||||
|
style: TextStyle(fontSize: 13, color: colors.onPrimary)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@@ -525,10 +588,12 @@ class _MdReaderTabPageState extends State<MdReaderTabPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: const Color(0xFFDDDDDD)),
|
border: Border.all(color: colors.onSurface.withValues(alpha: 0.15)),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Text('更换目录', style: TextStyle(fontSize: 13, color: Color(0xFF888888))),
|
child: Text('更换目录',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -50,10 +50,11 @@ class _MdViewerPageState extends State<MdViewerPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final fileName = widget.filePath.split('/').last;
|
final fileName = widget.filePath.split('/').last;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -61,69 +62,71 @@ class _MdViewerPageState extends State<MdViewerPage> {
|
|||||||
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
|
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: _buildBody(),
|
body: _buildBody(colors),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody(ColorScheme colors) {
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator(color: Color(0xFF1A1A1A)));
|
return Center(child: CircularProgressIndicator(color: colors.primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_error != null) {
|
if (_error != null) {
|
||||||
return _buildErrorState();
|
return _buildErrorState(colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Markdown(
|
return Markdown(
|
||||||
data: _content,
|
data: _content,
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
styleSheet: MarkdownStyleSheet(
|
styleSheet: MarkdownStyleSheet(
|
||||||
h1: const TextStyle(
|
h1: TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
h2: const TextStyle(
|
h2: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
h3: const TextStyle(
|
h3: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
p: const TextStyle(
|
p: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.8,
|
height: 1.8,
|
||||||
),
|
),
|
||||||
code: const TextStyle(
|
code: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
backgroundColor: Color(0xFFF5F5F5),
|
backgroundColor: colors.surfaceContainerHighest,
|
||||||
),
|
),
|
||||||
codeblockDecoration: BoxDecoration(
|
codeblockDecoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
border: Border.all(color: colors.outline),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
codeblockPadding: const EdgeInsets.all(12),
|
codeblockPadding: const EdgeInsets.all(12),
|
||||||
blockquote: const TextStyle(
|
blockquote: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
blockquoteDecoration: const BoxDecoration(
|
blockquoteDecoration: BoxDecoration(
|
||||||
border: Border(left: BorderSide(color: Color(0xFF999999), width: 4)),
|
border: Border(
|
||||||
|
left: BorderSide(
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.4), width: 4)),
|
||||||
),
|
),
|
||||||
blockquotePadding: const EdgeInsets.only(left: 12),
|
blockquotePadding: const EdgeInsets.only(left: 12),
|
||||||
listBullet: const TextStyle(
|
listBullet: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
listIndent: 24,
|
listIndent: 24,
|
||||||
a: const TextStyle(
|
a: const TextStyle(
|
||||||
@@ -133,12 +136,12 @@ class _MdViewerPageState extends State<MdViewerPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// ignore: deprecated_member_use
|
// ignore: deprecated_member_use
|
||||||
imageBuilder: (uri, title, alt) => _buildImage(uri.toString(), alt),
|
imageBuilder: (uri, title, alt) => _buildImage(colors, uri.toString(), alt),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建图片显示
|
/// 构建图片显示
|
||||||
Widget _buildImage(String uri, String? alt) {
|
Widget _buildImage(ColorScheme colors, String uri, String? alt) {
|
||||||
if (uri.isEmpty) return const SizedBox.shrink();
|
if (uri.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
// 处理相对路径:基于 md 文件所在目录
|
// 处理相对路径:基于 md 文件所在目录
|
||||||
@@ -158,17 +161,19 @@ class _MdViewerPageState extends State<MdViewerPage> {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.broken_image_outlined, size: 20, color: Color(0xFF999999)),
|
Icon(Icons.broken_image_outlined,
|
||||||
|
size: 20, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
alt ?? '图片加载失败',
|
alt ?? '图片加载失败',
|
||||||
style: const TextStyle(fontSize: 13, color: Color(0xFF999999)),
|
style:
|
||||||
|
TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -179,16 +184,17 @@ class _MdViewerPageState extends State<MdViewerPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildErrorState() {
|
Widget _buildErrorState(ColorScheme colors) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.error_outline, size: 48, color: Color(0xFFCCCCCC)),
|
Icon(Icons.error_outline,
|
||||||
|
size: 48, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
_error!,
|
_error!,
|
||||||
style: const TextStyle(fontSize: 14, color: Color(0xFF999999)),
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import 'package:webview_flutter/webview_flutter.dart';
|
|||||||
/// 豆瓣影视WebView页面 - 用于抓取影视信息
|
/// 豆瓣影视WebView页面 - 用于抓取影视信息
|
||||||
class DoubanWebViewPage extends StatefulWidget {
|
class DoubanWebViewPage extends StatefulWidget {
|
||||||
final String url;
|
final String url;
|
||||||
|
|
||||||
const DoubanWebViewPage({super.key, required this.url});
|
const DoubanWebViewPage({super.key, required this.url});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DoubanWebViewPage> createState() => _DoubanWebViewPageState();
|
State<DoubanWebViewPage> createState() => _DoubanWebViewPageState();
|
||||||
}
|
}
|
||||||
@@ -16,21 +16,21 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
late WebViewController _controller;
|
late WebViewController _controller;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
bool _canExtract = false;
|
bool _canExtract = false;
|
||||||
bool _isExtracting = false; // 防止重复提取
|
bool _isExtracting = false; // 防止重复提取
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initWebView();
|
_initWebView();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// 清理 WebView 资源
|
// 清理 WebView 资源
|
||||||
_controller.loadRequest(Uri.parse('about:blank'));
|
_controller.loadRequest(Uri.parse('about:blank'));
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initWebView() {
|
void _initWebView() {
|
||||||
_controller = WebViewController()
|
_controller = WebViewController()
|
||||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||||
@@ -55,23 +55,26 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
)
|
)
|
||||||
..loadRequest(Uri.parse(widget.url));
|
..loadRequest(Uri.parse(widget.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('豆瓣影视'),
|
title: const Text('豆瓣影视'),
|
||||||
leading: _buildBackButton(),
|
leading: _buildBackButton(),
|
||||||
actions: [
|
actions: [
|
||||||
// 提取按钮 - 始终显示
|
// 提取按钮 - 始终显示
|
||||||
_buildActionButton(
|
_buildActionButton(
|
||||||
|
colors: colors,
|
||||||
icon: Icons.auto_fix_high_outlined,
|
icon: Icons.auto_fix_high_outlined,
|
||||||
onPressed: _showExtractedInfo,
|
onPressed: _showExtractedInfo,
|
||||||
tooltip: '提取信息',
|
tooltip: '提取信息',
|
||||||
),
|
),
|
||||||
// 刷新按钮
|
// 刷新按钮
|
||||||
_buildActionButton(
|
_buildActionButton(
|
||||||
|
colors: colors,
|
||||||
icon: Icons.refresh,
|
icon: Icons.refresh,
|
||||||
onPressed: () => _controller.reload(),
|
onPressed: () => _controller.reload(),
|
||||||
tooltip: '刷新',
|
tooltip: '刷新',
|
||||||
@@ -83,16 +86,14 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
children: [
|
children: [
|
||||||
WebViewWidget(controller: _controller),
|
WebViewWidget(controller: _controller),
|
||||||
// 加载指示器
|
// 加载指示器
|
||||||
if (_isLoading)
|
if (_isLoading) const Center(
|
||||||
const Center(
|
child: CircularProgressIndicator(),
|
||||||
child: CircularProgressIndicator(),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建返回按钮
|
/// 构建返回按钮
|
||||||
Widget _buildBackButton() {
|
Widget _buildBackButton() {
|
||||||
return Container(
|
return Container(
|
||||||
@@ -121,6 +122,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
|
|
||||||
/// 构建右上角操作按钮
|
/// 构建右上角操作按钮
|
||||||
Widget _buildActionButton({
|
Widget _buildActionButton({
|
||||||
|
required ColorScheme colors,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required VoidCallback onPressed,
|
required VoidCallback onPressed,
|
||||||
required String tooltip,
|
required String tooltip,
|
||||||
@@ -138,75 +140,86 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Icon(icon, color: const Color(0xFF1A1A1A), size: 22),
|
child: Icon(icon, color: colors.onSurface, size: 22),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示提取的信息对话框
|
/// 显示提取的信息对话框
|
||||||
Future<void> _showExtractedInfo() async {
|
Future<void> _showExtractedInfo() async {
|
||||||
// 先提取信息
|
// 先提取信息
|
||||||
final movieInfo = await _extractMovieInfo();
|
final movieInfo = await _extractMovieInfo();
|
||||||
if (movieInfo == null) return;
|
if (movieInfo == null) return;
|
||||||
|
|
||||||
// 显示提取的信息
|
// 显示提取的信息
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (ctx) {
|
||||||
backgroundColor: Colors.white,
|
final colors = Theme.of(ctx).colorScheme;
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
return AlertDialog(
|
||||||
title: const Text(
|
backgroundColor: colors.surface,
|
||||||
'提取的影视信息',
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
style: TextStyle(
|
title: Text(
|
||||||
fontSize: 18,
|
'提取的影视信息',
|
||||||
fontWeight: FontWeight.w600,
|
style: TextStyle(
|
||||||
color: Color(0xFF1A1A1A),
|
fontSize: 18,
|
||||||
),
|
fontWeight: FontWeight.w600,
|
||||||
),
|
color: colors.onSurface,
|
||||||
content: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildInfoRow('标题', movieInfo['title']?.toString() ?? '未提取到'),
|
|
||||||
_buildInfoRow('导演', movieInfo['director']?.toString() ?? '未提取到'),
|
|
||||||
_buildInfoRow('类型', movieInfo['genres']?.toString() ?? '未提取到'),
|
|
||||||
_buildInfoRow('上映日期', movieInfo['releaseDate']?.toString() ?? '未提取到'),
|
|
||||||
if (movieInfo['summary'] != null)
|
|
||||||
_buildInfoRow('简介', movieInfo['summary'].toString().substring(0,
|
|
||||||
movieInfo['summary'].toString().length > 100 ? 100 : movieInfo['summary'].toString().length) + '...'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: const Text(
|
|
||||||
'取消',
|
|
||||||
style: TextStyle(color: Color(0xFF999999)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
content: SingleChildScrollView(
|
||||||
onPressed: () {
|
child: Column(
|
||||||
Navigator.pop(context);
|
mainAxisSize: MainAxisSize.min,
|
||||||
Navigator.pop(context, movieInfo);
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
},
|
children: [
|
||||||
child: const Text(
|
_buildInfoRow(colors, '标题', movieInfo['title']?.toString() ?? '未提取到'),
|
||||||
'使用此信息',
|
_buildInfoRow(colors, '导演', movieInfo['director']?.toString() ?? '未提取到'),
|
||||||
style: TextStyle(color: Color(0xFF1A1A1A), fontWeight: FontWeight.w600),
|
_buildInfoRow(colors, '类型', movieInfo['genres']?.toString() ?? '未提取到'),
|
||||||
|
_buildInfoRow(colors, '上映日期', movieInfo['releaseDate']?.toString() ?? '未提取到'),
|
||||||
|
if (movieInfo['summary'] != null)
|
||||||
|
_buildInfoRow(
|
||||||
|
colors,
|
||||||
|
'简介',
|
||||||
|
movieInfo['summary'].toString().substring(
|
||||||
|
0,
|
||||||
|
movieInfo['summary'].toString().length > 100
|
||||||
|
? 100
|
||||||
|
: movieInfo['summary'].toString().length) +
|
||||||
|
'...'),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
actions: [
|
||||||
),
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: Text(
|
||||||
|
'取消',
|
||||||
|
style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
Navigator.pop(context, movieInfo);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'使用此信息',
|
||||||
|
style: TextStyle(
|
||||||
|
color: colors.onSurface, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建信息行
|
/// 构建信息行
|
||||||
Widget _buildInfoRow(String label, String value) {
|
Widget _buildInfoRow(ColorScheme colors, String label, String value) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -216,18 +229,18 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
width: 64,
|
width: 64,
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
value,
|
value,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -240,10 +253,10 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
Future<Map<String, dynamic>?> _extractMovieInfo() async {
|
Future<Map<String, dynamic>?> _extractMovieInfo() async {
|
||||||
// 检查是否已提取过,避免重复点击
|
// 检查是否已提取过,避免重复点击
|
||||||
if (_isExtracting) return null;
|
if (_isExtracting) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_isExtracting = true;
|
_isExtracting = true;
|
||||||
|
|
||||||
// 显示加载提示
|
// 显示加载提示
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -252,16 +265,16 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 执行JavaScript代码提取页面信息
|
// 执行JavaScript代码提取页面信息
|
||||||
final result = await _controller.runJavaScriptReturningResult(r'''
|
final result = await _controller.runJavaScriptReturningResult(r'''
|
||||||
(function() {
|
(function() {
|
||||||
const info = {};
|
const info = {};
|
||||||
|
|
||||||
// 获取标题 - 移动版页面
|
// 获取标题 - 移动版页面
|
||||||
const titleEl = document.querySelector('.sub-title');
|
const titleEl = document.querySelector('.sub-title');
|
||||||
info.title = titleEl ? titleEl.textContent.trim() : '';
|
info.title = titleEl ? titleEl.textContent.trim() : '';
|
||||||
|
|
||||||
// 获取年份 - 从 original-title 中提取
|
// 获取年份 - 从 original-title 中提取
|
||||||
const originalTitleEl = document.querySelector('.sub-original-title');
|
const originalTitleEl = document.querySelector('.sub-original-title');
|
||||||
if (originalTitleEl) {
|
if (originalTitleEl) {
|
||||||
@@ -270,7 +283,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
} else {
|
} else {
|
||||||
info.year = '';
|
info.year = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取封面图 - 从 sub-cover 中的 img 标签获取
|
// 获取封面图 - 从 sub-cover 中的 img 标签获取
|
||||||
const coverEl = document.querySelector('.sub-cover img');
|
const coverEl = document.querySelector('.sub-cover img');
|
||||||
if (coverEl) {
|
if (coverEl) {
|
||||||
@@ -283,11 +296,11 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
} else {
|
} else {
|
||||||
info.coverUrl = '';
|
info.coverUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取评分 - 移动版可能在 mark-item 中
|
// 获取评分 - 移动版可能在 mark-item 中
|
||||||
const ratingEl = document.querySelector('.rating-num') || document.querySelector('.score');
|
const ratingEl = document.querySelector('.rating-num') || document.querySelector('.score');
|
||||||
info.rating = ratingEl ? ratingEl.textContent.trim() : '';
|
info.rating = ratingEl ? ratingEl.textContent.trim() : '';
|
||||||
|
|
||||||
// 获取导演 - 从演职员列表中找
|
// 获取导演 - 从演职员列表中找
|
||||||
const directorEl = document.querySelector('.movie-celebrities .item__celebrity .role');
|
const directorEl = document.querySelector('.movie-celebrities .item__celebrity .role');
|
||||||
if (directorEl && directorEl.textContent.includes('导演')) {
|
if (directorEl && directorEl.textContent.includes('导演')) {
|
||||||
@@ -296,7 +309,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
} else {
|
} else {
|
||||||
info.director = '';
|
info.director = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取编剧 - 从演职员列表中找(匹配"编剧"或"剧本")
|
// 获取编剧 - 从演职员列表中找(匹配"编剧"或"剧本")
|
||||||
const writerEls = document.querySelectorAll('.movie-celebrities .item__celebrity');
|
const writerEls = document.querySelectorAll('.movie-celebrities .item__celebrity');
|
||||||
const writers = [];
|
const writers = [];
|
||||||
@@ -308,7 +321,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
info.writers = writers;
|
info.writers = writers;
|
||||||
|
|
||||||
// 获取主演- 从演职员列表中找前5个
|
// 获取主演- 从演职员列表中找前5个
|
||||||
const actorEls = document.querySelectorAll('.movie-celebrities .item__celebrity');
|
const actorEls = document.querySelectorAll('.movie-celebrities .item__celebrity');
|
||||||
const actors = [];
|
const actors = [];
|
||||||
@@ -316,10 +329,10 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
const roleEl = el
|
const roleEl = el
|
||||||
.querySelector('.role');
|
.querySelector('.role');
|
||||||
if (roleEl && (
|
if (roleEl && (
|
||||||
roleEl.textContent.includes('配音') ||
|
roleEl.textContent.includes('配音') ||
|
||||||
roleEl.textContent.includes('主演') ||
|
roleEl.textContent.includes('主演') ||
|
||||||
roleEl.textContent.includes('演员') ||
|
roleEl.textContent.includes('演员') ||
|
||||||
roleEl.textContent.includes('参演') ||
|
roleEl.textContent.includes('参演') ||
|
||||||
roleEl.textContent.includes('饰')
|
roleEl.textContent.includes('饰')
|
||||||
)) {
|
)) {
|
||||||
const nameEl = el.querySelector('.name');
|
const nameEl = el.querySelector('.name');
|
||||||
@@ -327,20 +340,20 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
info.actors = actors;
|
info.actors = actors;
|
||||||
|
|
||||||
// 获取类型 - 从 sub-meta 或标签中提取
|
// 获取类型 - 从 sub-meta 或标签中提取
|
||||||
const metaEl = document.querySelector('.sub-meta');
|
const metaEl = document.querySelector('.sub-meta');
|
||||||
if (metaEl) {
|
if (metaEl) {
|
||||||
const metaText = metaEl.textContent;
|
const metaText = metaEl.textContent;
|
||||||
const parts = metaText.split('/').map(s => s.trim());
|
const parts = metaText.split('/').map(s => s.trim());
|
||||||
// 过滤出类型(通常是中文,不是日期,不是时长)
|
// 过滤出类型(通常是中文,不是日期,不是时长)
|
||||||
info.genres = parts.filter(p =>
|
info.genres = parts.filter(p =>
|
||||||
p && !p.match(/^\d{4}/) && !p.includes('分钟') && !p.includes('上映')
|
p && !p.match(/^\d{4}/) && !p.includes('分钟') && !p.includes('上映')
|
||||||
).join(',');
|
).join(',');
|
||||||
} else {
|
} else {
|
||||||
info.genres = '';
|
info.genres = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取上映日期
|
// 获取上映日期
|
||||||
if (metaEl) {
|
if (metaEl) {
|
||||||
const dateMatch = metaEl.textContent.match(/(\d{4}-\d{2}-\d{2})/);
|
const dateMatch = metaEl.textContent.match(/(\d{4}-\d{2}-\d{2})/);
|
||||||
@@ -348,7 +361,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
} else {
|
} else {
|
||||||
info.releaseDate = '';
|
info.releaseDate = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取简介
|
// 获取简介
|
||||||
const summaryEl = document.querySelector('.subject-intro p');
|
const summaryEl = document.querySelector('.subject-intro p');
|
||||||
if (summaryEl) {
|
if (summaryEl) {
|
||||||
@@ -356,7 +369,7 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
} else {
|
} else {
|
||||||
info.summary = '';
|
info.summary = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取别名 - 从 original-title 中提取(去掉年份)
|
// 获取别名 - 从 original-title 中提取(去掉年份)
|
||||||
if (originalTitleEl) {
|
if (originalTitleEl) {
|
||||||
const fullText = originalTitleEl.textContent.trim();
|
const fullText = originalTitleEl.textContent.trim();
|
||||||
@@ -364,14 +377,14 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
} else {
|
} else {
|
||||||
info.alternateTitles = [];
|
info.alternateTitles = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.stringify(info);
|
return JSON.stringify(info);
|
||||||
})()
|
})()
|
||||||
''');
|
''');
|
||||||
|
|
||||||
// 关闭加载提示
|
// 关闭加载提示
|
||||||
if (mounted) Navigator.pop(context);
|
if (mounted) Navigator.pop(context);
|
||||||
|
|
||||||
// 解析提取的信息
|
// 解析提取的信息
|
||||||
// result 是 JavaScript 执行结果,已经是 JSON 字符串(带引号的)
|
// result 是 JavaScript 执行结果,已经是 JSON 字符串(带引号的)
|
||||||
final String jsonStr = result.toString();
|
final String jsonStr = result.toString();
|
||||||
@@ -380,12 +393,12 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
|
|||||||
? jsonDecode(jsonStr) as String
|
? jsonDecode(jsonStr) as String
|
||||||
: jsonStr;
|
: jsonStr;
|
||||||
final Map<String, dynamic> movieInfo = jsonDecode(cleanJson) as Map<String, dynamic>;
|
final Map<String, dynamic> movieInfo = jsonDecode(cleanJson) as Map<String, dynamic>;
|
||||||
|
|
||||||
return movieInfo;
|
return movieInfo;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 关闭加载提示
|
// 关闭加载提示
|
||||||
if (mounted) Navigator.pop(context);
|
if (mounted) Navigator.pop(context);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('提取信息失败: $e')),
|
SnackBar(content: Text('提取信息失败: $e')),
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import 'movie_share_page.dart';
|
|||||||
/// 影视详情页 - 极简主义设计
|
/// 影视详情页 - 极简主义设计
|
||||||
class MovieDetailPage extends StatefulWidget {
|
class MovieDetailPage extends StatefulWidget {
|
||||||
final Movie movie;
|
final Movie movie;
|
||||||
|
|
||||||
const MovieDetailPage({super.key, required this.movie});
|
const MovieDetailPage({super.key, required this.movie});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MovieDetailPage> createState() => _MovieDetailPageState();
|
State<MovieDetailPage> createState() => _MovieDetailPageState();
|
||||||
}
|
}
|
||||||
@@ -27,77 +27,53 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
// 页面获得焦点时刷新数据
|
|
||||||
_refreshMovieData();
|
_refreshMovieData();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _refreshMovieData() {
|
void _refreshMovieData() {
|
||||||
final provider = context.read<AppProvider>();
|
final provider = context.read<AppProvider>();
|
||||||
// 强制刷新当前影视数据
|
|
||||||
provider.loadMovies();
|
provider.loadMovies();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// 从 Provider 获取最新的 movie 数据,实现动态刷新
|
final colors = Theme.of(context).colorScheme;
|
||||||
final movie = context.watch<AppProvider>().movies
|
final movie = context.watch<AppProvider>().movies
|
||||||
.where((m) => m.id == widget.movie.id)
|
.where((m) => m.id == widget.movie.id)
|
||||||
.firstOrNull ?? widget.movie;
|
.firstOrNull ?? widget.movie;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
CustomScrollView(
|
CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
// 顶部海报区域
|
|
||||||
_buildSliverAppBar(movie),
|
_buildSliverAppBar(movie),
|
||||||
|
|
||||||
// 内容区域
|
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 基本信息
|
|
||||||
_buildBasicInfo(movie),
|
_buildBasicInfo(movie),
|
||||||
|
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
|
||||||
|
|
||||||
// 导演
|
|
||||||
if (movie.directors.isNotEmpty)
|
if (movie.directors.isNotEmpty)
|
||||||
_buildDirectorsSection(movie),
|
_buildDirectorsSection(movie),
|
||||||
|
|
||||||
// 编剧
|
|
||||||
if (movie.writers.isNotEmpty)
|
if (movie.writers.isNotEmpty)
|
||||||
_buildWritersSection(movie),
|
_buildWritersSection(movie),
|
||||||
|
|
||||||
// 主演
|
|
||||||
if (movie.actors.isNotEmpty)
|
if (movie.actors.isNotEmpty)
|
||||||
_buildActorsSection(movie),
|
_buildActorsSection(movie),
|
||||||
|
|
||||||
// 类型
|
|
||||||
if (movie.genres.isNotEmpty)
|
if (movie.genres.isNotEmpty)
|
||||||
_buildGenresSection(movie),
|
_buildGenresSection(movie),
|
||||||
|
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
|
||||||
|
|
||||||
// 简介
|
|
||||||
if (movie.summary != null && movie.summary!.isNotEmpty)
|
if (movie.summary != null && movie.summary!.isNotEmpty)
|
||||||
_buildSummarySection(movie),
|
_buildSummarySection(movie),
|
||||||
|
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
|
||||||
|
|
||||||
// 影评和海报墙入口
|
|
||||||
_buildExtraSections(movie),
|
_buildExtraSections(movie),
|
||||||
|
|
||||||
const SizedBox(height: 120),
|
const SizedBox(height: 120),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// 右下角悬浮按钮组
|
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 16,
|
right: 16,
|
||||||
bottom: 24,
|
bottom: 24,
|
||||||
@@ -107,9 +83,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建右下角悬浮按钮组
|
|
||||||
Widget _buildFloatingActionButtons(Movie movie) {
|
Widget _buildFloatingActionButtons(Movie movie) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -117,13 +93,16 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
icon: Icons.edit_outlined,
|
icon: Icons.edit_outlined,
|
||||||
onPressed: () => _navigateToEdit(context),
|
onPressed: () => _navigateToEdit(context),
|
||||||
tooltip: '编辑',
|
tooltip: '编辑',
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
foregroundColor: colors.onPrimary,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildFloatingButton(
|
_buildFloatingButton(
|
||||||
icon: Icons.delete_outline,
|
icon: Icons.delete_outline,
|
||||||
onPressed: () => _showDeleteDialog(context),
|
onPressed: () => _showDeleteDialog(context),
|
||||||
tooltip: '删除',
|
tooltip: '删除',
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: colors.error,
|
||||||
|
foregroundColor: colors.onError,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildFloatingButton(
|
_buildFloatingButton(
|
||||||
@@ -131,17 +110,18 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
onPressed: () => _showSharePoster(movie),
|
onPressed: () => _showSharePoster(movie),
|
||||||
tooltip: '分享海报',
|
tooltip: '分享海报',
|
||||||
backgroundColor: const Color(0xFF4CAF50),
|
backgroundColor: const Color(0xFF4CAF50),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建单个悬浮按钮
|
|
||||||
Widget _buildFloatingButton({
|
Widget _buildFloatingButton({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required VoidCallback onPressed,
|
required VoidCallback onPressed,
|
||||||
required String tooltip,
|
required String tooltip,
|
||||||
Color backgroundColor = const Color(0xFF1A1A1A),
|
required Color backgroundColor,
|
||||||
|
required Color foregroundColor,
|
||||||
}) {
|
}) {
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
@@ -160,15 +140,15 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(icon, size: 18, color: Colors.white),
|
icon: Icon(icon, size: 18, color: foregroundColor),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
tooltip: tooltip,
|
tooltip: tooltip,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建带背景的返回按钮
|
|
||||||
Widget _buildBackButton() {
|
Widget _buildBackButton() {
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||||
@@ -194,21 +174,19 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建顶部 AppBar
|
|
||||||
Widget _buildSliverAppBar(Movie movie) {
|
Widget _buildSliverAppBar(Movie movie) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
expandedHeight: 320,
|
expandedHeight: 320,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: colors.surfaceContainerHighest,
|
||||||
leading: _buildBackButton(),
|
leading: _buildBackButton(),
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
background: _buildPosterSection(movie),
|
background: _buildPosterSection(movie),
|
||||||
),
|
),
|
||||||
// 右上角按钮已移到右下角悬浮按钮
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建海报区域
|
|
||||||
Widget _buildPosterSection(Movie movie) {
|
Widget _buildPosterSection(Movie movie) {
|
||||||
return SizedBox.expand(
|
return SizedBox.expand(
|
||||||
child: movie.posterPath != null && movie.posterPath!.isNotEmpty
|
child: movie.posterPath != null && movie.posterPath!.isNotEmpty
|
||||||
@@ -220,79 +198,74 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
: _buildPosterPlaceholder(),
|
: _buildPosterPlaceholder(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPosterPlaceholder() {
|
Widget _buildPosterPlaceholder() {
|
||||||
return const Center(
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.movie_outlined,
|
Icons.movie_outlined,
|
||||||
size: 64,
|
size: 64,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'暂无海报',
|
'暂无海报',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建基本信息
|
|
||||||
Widget _buildBasicInfo(Movie movie) {
|
Widget _buildBasicInfo(Movie movie) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 影视名称
|
|
||||||
Text(
|
Text(
|
||||||
movie.title,
|
movie.title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.3,
|
height: 1.3,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 别名(显示在主名称下面,用 / 分隔)
|
|
||||||
if (movie.alternateTitles.isNotEmpty) ...[
|
if (movie.alternateTitles.isNotEmpty) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
movie.alternateTitles.join(' / '),
|
movie.alternateTitles.join(' / '),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 评分和状态
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (movie.rating != null) ...[
|
if (movie.rating != null) ...[
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.star,
|
Icons.star,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
movie.rating!.toStringAsFixed(1),
|
movie.rating!.toStringAsFixed(1),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
@@ -300,62 +273,56 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
_buildStatusTag(movie),
|
_buildStatusTag(movie),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// 上映日期
|
|
||||||
if (movie.releaseDate != null)
|
if (movie.releaseDate != null)
|
||||||
Text(
|
Text(
|
||||||
'${movie.releaseDate!.year}年${movie.releaseDate!.month.toString().padLeft(2, '0')}月上映',
|
'${movie.releaseDate!.year}年${movie.releaseDate!.month.toString().padLeft(2, '0')}月上映',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// 观看日期
|
|
||||||
if (movie.watchDate != null)
|
if (movie.watchDate != null)
|
||||||
Text(
|
Text(
|
||||||
'观看于 ${_formatDate(movie.watchDate!)}',
|
'观看于 ${_formatDate(movie.watchDate!)}',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建状态标签
|
|
||||||
Widget _buildStatusTag(Movie movie) {
|
Widget _buildStatusTag(Movie movie) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
String label;
|
String label;
|
||||||
Color bgColor;
|
Color bgColor;
|
||||||
Color textColor;
|
Color textColor;
|
||||||
switch (movie.status) {
|
switch (movie.status) {
|
||||||
case 'watched':
|
case 'watched':
|
||||||
label = '已看';
|
label = '已看';
|
||||||
bgColor = const Color(0xFF1A1A1A);
|
bgColor = colors.primary;
|
||||||
textColor = Colors.white;
|
textColor = colors.onPrimary;
|
||||||
break;
|
break;
|
||||||
case 'watching':
|
case 'watching':
|
||||||
label = '在看';
|
label = '在看';
|
||||||
bgColor = const Color(0xFFF0F0F0);
|
bgColor = colors.outlineVariant;
|
||||||
textColor = const Color(0xFF666666);
|
textColor = colors.onSurface.withValues(alpha: 0.6);
|
||||||
break;
|
break;
|
||||||
case 'want_to_watch':
|
case 'want_to_watch':
|
||||||
label = '想看';
|
label = '想看';
|
||||||
bgColor = const Color(0xFFF5F5F5);
|
bgColor = colors.surfaceContainerHighest;
|
||||||
textColor = const Color(0xFF999999);
|
textColor = colors.onSurface.withValues(alpha: 0.4);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
label = '未知';
|
label = '未知';
|
||||||
bgColor = const Color(0xFFEEEEEE);
|
bgColor = colors.outlineVariant;
|
||||||
textColor = const Color(0xFFCCCCCC);
|
textColor = colors.onSurface.withValues(alpha: 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -372,30 +339,30 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建导演区域
|
|
||||||
Widget _buildDirectorsSection(Movie movie) {
|
Widget _buildDirectorsSection(Movie movie) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 48,
|
width: 48,
|
||||||
child: Text(
|
child: Text(
|
||||||
'导演',
|
'导演',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
movie.directors.join(','),
|
movie.directors.join(','),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -405,29 +372,29 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建编剧区域
|
|
||||||
Widget _buildWritersSection(Movie movie) {
|
Widget _buildWritersSection(Movie movie) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 48,
|
width: 48,
|
||||||
child: Text(
|
child: Text(
|
||||||
'编剧',
|
'编剧',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
movie.writers.join(','),
|
movie.writers.join(','),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -437,29 +404,29 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建主演区域
|
|
||||||
Widget _buildActorsSection(Movie movie) {
|
Widget _buildActorsSection(Movie movie) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 48,
|
width: 48,
|
||||||
child: Text(
|
child: Text(
|
||||||
'主演',
|
'主演',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
movie.actors.join(','),
|
movie.actors.join(','),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -469,20 +436,20 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建类型区域
|
|
||||||
Widget _buildGenresSection(Movie movie) {
|
Widget _buildGenresSection(Movie movie) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 48,
|
width: 48,
|
||||||
child: Text(
|
child: Text(
|
||||||
'类型',
|
'类型',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -494,14 +461,14 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
genre,
|
genre,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -512,9 +479,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建简介区域
|
|
||||||
Widget _buildSummarySection(Movie movie) {
|
Widget _buildSummarySection(Movie movie) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -526,17 +493,17 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
width: 4,
|
width: 4,
|
||||||
height: 16,
|
height: 16,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text(
|
Text(
|
||||||
'简介',
|
'简介',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -545,14 +512,14 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
movie.summary!,
|
movie.summary!,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.8,
|
height: 1.8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -561,9 +528,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建额外功能区域(影评、海报墙)
|
|
||||||
Widget _buildExtraSections(Movie movie) {
|
Widget _buildExtraSections(Movie movie) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -575,23 +542,22 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
width: 4,
|
width: 4,
|
||||||
height: 16,
|
height: 16,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text(
|
Text(
|
||||||
'更多',
|
'更多',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 影评入口
|
|
||||||
_buildExtraSectionItem(
|
_buildExtraSectionItem(
|
||||||
icon: Icons.rate_review_outlined,
|
icon: Icons.rate_review_outlined,
|
||||||
title: '影评',
|
title: '影评',
|
||||||
@@ -601,7 +567,6 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
onTap: () => _navigateToReviews(movie),
|
onTap: () => _navigateToReviews(movie),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// 海报墙入口
|
|
||||||
_buildExtraSectionItem(
|
_buildExtraSectionItem(
|
||||||
icon: Icons.photo_library_outlined,
|
icon: Icons.photo_library_outlined,
|
||||||
title: '海报墙',
|
title: '海报墙',
|
||||||
@@ -615,7 +580,6 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建更多区域项
|
|
||||||
Widget _buildExtraSectionItem({
|
Widget _buildExtraSectionItem({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String title,
|
required String title,
|
||||||
@@ -624,14 +588,15 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
required String unit,
|
required String unit,
|
||||||
required VoidCallback onTap,
|
required VoidCallback onTap,
|
||||||
}) {
|
}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -639,14 +604,14 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: const Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@@ -656,10 +621,10 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
@@ -669,9 +634,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
final count = snapshot.data ?? 0;
|
final count = snapshot.data ?? 0;
|
||||||
return Text(
|
return Text(
|
||||||
count > 0 ? '$count $unit' : emptyText,
|
count > 0 ? '$count $unit' : emptyText,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -679,16 +644,16 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.chevron_right,
|
Icons.chevron_right,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _navigateToReviews(Movie movie) {
|
void _navigateToReviews(Movie movie) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -697,7 +662,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _navigateToPosters(Movie movie) {
|
void _navigateToPosters(Movie movie) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -706,39 +671,38 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 格式化日期
|
|
||||||
String _formatDate(DateTime date) {
|
String _formatDate(DateTime date) {
|
||||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 跳转到编辑页面
|
|
||||||
void _navigateToEdit(BuildContext context) {
|
void _navigateToEdit(BuildContext context) {
|
||||||
Navigator.pushNamed(context, '/movie-form', arguments: widget.movie).then((_) {
|
Navigator.pushNamed(context, '/movie-form', arguments: widget.movie).then((_) {
|
||||||
context.read<AppProvider>().loadMovies();
|
context.read<AppProvider>().loadMovies();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示删除对话框
|
|
||||||
void _showDeleteDialog(BuildContext context) {
|
void _showDeleteDialog(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'确认删除',
|
'确认删除',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
content: Text(
|
content: Text(
|
||||||
'确定要删除"${widget.movie.title}"吗?删除后可在回收站恢复。',
|
'确定要删除"${widget.movie.title}"吗?删除后可在回收站恢复。',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -746,7 +710,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFF666666),
|
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
child: const Text('取消'),
|
child: const Text('取消'),
|
||||||
@@ -760,8 +724,8 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
ToastUtil.show(context, '已删除');
|
ToastUtil.show(context, '已删除');
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: colors.error,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colors.onError,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -775,18 +739,14 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 请求存储权限
|
|
||||||
Future<bool> _requestStoragePermission() async {
|
Future<bool> _requestStoragePermission() async {
|
||||||
// Android 13+ 使用新的权限
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
final sdkInt = await _getAndroidSdkInt();
|
final sdkInt = await _getAndroidSdkInt();
|
||||||
if (sdkInt >= 33) {
|
if (sdkInt >= 33) {
|
||||||
// Android 13+ 使用 READ_MEDIA_IMAGES
|
|
||||||
final status = await Permission.photos.request();
|
final status = await Permission.photos.request();
|
||||||
return status.isGranted;
|
return status.isGranted;
|
||||||
} else {
|
} else {
|
||||||
// Android 12 及以下使用存储权限
|
|
||||||
var status = await Permission.storage.request();
|
var status = await Permission.storage.request();
|
||||||
if (status.isDenied) {
|
if (status.isDenied) {
|
||||||
status = await Permission.storage.request();
|
status = await Permission.storage.request();
|
||||||
@@ -794,18 +754,13 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
|||||||
return status.isGranted;
|
return status.isGranted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// iOS 不需要额外权限来保存到应用沙盒
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取 Android SDK 版本
|
|
||||||
Future<int> _getAndroidSdkInt() async {
|
Future<int> _getAndroidSdkInt() async {
|
||||||
// 简化处理,实际可以通过 platform channel 获取
|
|
||||||
// 这里默认返回较低版本,使用传统存储权限
|
|
||||||
return 30;
|
return 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示分享海报页面
|
|
||||||
void _showSharePoster(Movie movie) {
|
void _showSharePoster(Movie movie) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -45,8 +45,9 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('海报墙'),
|
title: const Text('海报墙'),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -66,6 +67,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState() {
|
Widget _buildEmptyState() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -74,21 +76,21 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.photo_library_outlined,
|
Icons.photo_library_outlined,
|
||||||
size: 40,
|
size: 40,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Text(
|
Text(
|
||||||
'暂无海报',
|
'暂无海报',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -97,15 +99,15 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'添加记录',
|
'添加记录',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Colors.white,
|
color: colors.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -130,10 +132,11 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPosterItem(MoviePoster poster, int index) {
|
Widget _buildPosterItem(MoviePoster poster, int index) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
// 根据索引生成不同的高度,实现瀑布流效果
|
// 根据索引生成不同的高度,实现瀑布流效果
|
||||||
final heights = [180.0, 220.0, 160.0, 200.0, 240.0, 190.0];
|
final heights = [180.0, 220.0, 160.0, 200.0, 240.0, 190.0];
|
||||||
final height = heights[index % heights.length];
|
final height = heights[index % heights.length];
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => _showPosterDetail(poster),
|
onTap: () => _showPosterDetail(poster),
|
||||||
onLongPress: () => _showDeleteDialog(poster),
|
onLongPress: () => _showDeleteDialog(poster),
|
||||||
@@ -158,10 +161,10 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
Image.file(
|
Image.file(
|
||||||
File(poster.posterPath),
|
File(poster.posterPath),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) => const Center(
|
errorBuilder: (_, __, ___) => Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.broken_image,
|
Icons.broken_image,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -194,7 +197,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
void _showPosterDetail(MoviePoster poster) {
|
void _showPosterDetail(MoviePoster poster) {
|
||||||
// 找到当前海报的索引
|
// 找到当前海报的索引
|
||||||
final initialIndex = _posters.indexWhere((p) => p.id == poster.id);
|
final initialIndex = _posters.indexWhere((p) => p.id == poster.id);
|
||||||
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -210,61 +213,72 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
// 显示选择对话框
|
// 显示选择对话框
|
||||||
final result = await showModalBottomSheet<int>(
|
final result = await showModalBottomSheet<int>(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.transparent,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
),
|
),
|
||||||
builder: (context) => SafeArea(
|
builder: (context) {
|
||||||
child: Padding(
|
final colors = Theme.of(context).colorScheme;
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
return Container(
|
||||||
child: Column(
|
decoration: BoxDecoration(
|
||||||
mainAxisSize: MainAxisSize.min,
|
color: colors.surface,
|
||||||
children: [
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
// 顶部指示条
|
|
||||||
Container(
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFE0E0E0),
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
// 标题
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'添加海报',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF1A1A1A),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// 从相册选择
|
|
||||||
_buildAddOption(
|
|
||||||
icon: Icons.photo_library_outlined,
|
|
||||||
title: '从相册选择',
|
|
||||||
subtitle: '选择本地图片',
|
|
||||||
onTap: () => Navigator.pop(context, 0),
|
|
||||||
),
|
|
||||||
// 网络链接
|
|
||||||
_buildAddOption(
|
|
||||||
icon: Icons.link_outlined,
|
|
||||||
title: '网络链接',
|
|
||||||
subtitle: '输入图片URL地址',
|
|
||||||
onTap: () => Navigator.pop(context, 1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
child: SafeArea(
|
||||||
),
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 顶部指示条
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.outline,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// 标题
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'添加海报',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// 从相册选择
|
||||||
|
_buildAddOption(
|
||||||
|
colors: colors,
|
||||||
|
icon: Icons.photo_library_outlined,
|
||||||
|
title: '从相册选择',
|
||||||
|
subtitle: '选择本地图片',
|
||||||
|
onTap: () => Navigator.pop(context, 0),
|
||||||
|
),
|
||||||
|
// 网络链接
|
||||||
|
_buildAddOption(
|
||||||
|
colors: colors,
|
||||||
|
icon: Icons.link_outlined,
|
||||||
|
title: '网络链接',
|
||||||
|
subtitle: '输入图片URL地址',
|
||||||
|
onTap: () => Navigator.pop(context, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
@@ -289,10 +303,10 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
if (pickedFile != null) {
|
if (pickedFile != null) {
|
||||||
// 生成文件名
|
// 生成文件名
|
||||||
final fileName = 'posterimg_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
final fileName = 'posterimg_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||||
|
|
||||||
// 保存到 posterimgs 子目录: images/movies/{movieId}/posterimgs/{fileName}
|
// 保存到 posterimgs 子目录: images/movies/{movieId}/posterimgs/{fileName}
|
||||||
final targetPath = await ImagePathHelper.instance.getMoviePosterImgPath(
|
final targetPath = await ImagePathHelper.instance.getMoviePosterImgPath(
|
||||||
widget.movie.id,
|
widget.movie.id,
|
||||||
fileName
|
fileName
|
||||||
);
|
);
|
||||||
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
|
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
|
||||||
@@ -323,55 +337,58 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
/// 从网络链接添加
|
/// 从网络链接添加
|
||||||
Future<void> _pickFromUrl() async {
|
Future<void> _pickFromUrl() async {
|
||||||
final urlController = TextEditingController();
|
final urlController = TextEditingController();
|
||||||
|
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) {
|
||||||
backgroundColor: Colors.white,
|
final colors = Theme.of(context).colorScheme;
|
||||||
elevation: 0,
|
return AlertDialog(
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
backgroundColor: colors.surface,
|
||||||
title: const Text('添加网络图片'),
|
elevation: 0,
|
||||||
content: Column(
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
mainAxisSize: MainAxisSize.min,
|
title: const Text('添加网络图片'),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
content: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
const Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
'请输入图片链接地址',
|
children: [
|
||||||
style: TextStyle(
|
Text(
|
||||||
fontSize: 14,
|
'请输入图片链接地址',
|
||||||
color: Color(0xFF666666),
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: urlController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'https://example.com/image.jpg',
|
||||||
|
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
enabledBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: colors.outline),
|
||||||
|
),
|
||||||
|
focusedBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: colors.primary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
TextButton(
|
||||||
TextField(
|
onPressed: () => Navigator.pop(context, true),
|
||||||
controller: urlController,
|
child: Text('确定', style: TextStyle(color: colors.onSurface)),
|
||||||
decoration: const InputDecoration(
|
|
||||||
hintText: 'https://example.com/image.jpg',
|
|
||||||
hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
|
|
||||||
border: UnderlineInputBorder(),
|
|
||||||
enabledBorder: UnderlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: Color(0xFFE5E5E5)),
|
|
||||||
),
|
|
||||||
focusedBorder: UnderlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: Color(0xFF1A1A1A)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: const TextStyle(fontSize: 14),
|
|
||||||
keyboardType: TextInputType.url,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
actions: [
|
},
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, false),
|
|
||||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
child: const Text('确定', style: TextStyle(color: Color(0xFF1A1A1A))),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed != true) return;
|
if (confirmed != true) return;
|
||||||
@@ -404,7 +421,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
'Referer': Uri.parse(url).replace(path: '/').toString(),
|
'Referer': Uri.parse(url).replace(path: '/').toString(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('下载失败: HTTP ${response.statusCode}');
|
throw Exception('下载失败: HTTP ${response.statusCode}');
|
||||||
}
|
}
|
||||||
@@ -422,10 +439,10 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
|
|
||||||
// 生成文件名
|
// 生成文件名
|
||||||
final fileName = 'posterimg_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
final fileName = 'posterimg_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||||
|
|
||||||
// 保存到 posterimgs 子目录
|
// 保存到 posterimgs 子目录
|
||||||
final targetPath = await ImagePathHelper.instance.getMoviePosterImgPath(
|
final targetPath = await ImagePathHelper.instance.getMoviePosterImgPath(
|
||||||
widget.movie.id,
|
widget.movie.id,
|
||||||
fileName
|
fileName
|
||||||
);
|
);
|
||||||
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
|
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
|
||||||
@@ -453,6 +470,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
|
|
||||||
/// 构建添加选项
|
/// 构建添加选项
|
||||||
Widget _buildAddOption({
|
Widget _buildAddOption({
|
||||||
|
required ColorScheme colors,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String title,
|
required String title,
|
||||||
required String subtitle,
|
required String subtitle,
|
||||||
@@ -468,13 +486,13 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
width: 44,
|
width: 44,
|
||||||
height: 44,
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 22,
|
size: 22,
|
||||||
color: const Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
@@ -484,26 +502,26 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
subtitle,
|
subtitle,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.chevron_right,
|
Icons.chevron_right,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -515,28 +533,31 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
void _showDeleteDialog(MoviePoster poster) {
|
void _showDeleteDialog(MoviePoster poster) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) {
|
||||||
backgroundColor: Colors.white,
|
final colors = Theme.of(context).colorScheme;
|
||||||
elevation: 0,
|
return AlertDialog(
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
backgroundColor: colors.surface,
|
||||||
title: const Text('确认删除'),
|
elevation: 0,
|
||||||
content: const Text('确定要删除这张海报吗?'),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
actions: [
|
title: const Text('确认删除'),
|
||||||
TextButton(
|
content: const Text('确定要删除这张海报吗?'),
|
||||||
onPressed: () => Navigator.pop(context),
|
actions: [
|
||||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
TextButton(
|
||||||
),
|
onPressed: () => Navigator.pop(context),
|
||||||
TextButton(
|
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
onPressed: () async {
|
),
|
||||||
await context.read<AppProvider>().removeMoviePoster(poster.id);
|
TextButton(
|
||||||
Navigator.pop(context);
|
onPressed: () async {
|
||||||
_loadPosters();
|
await context.read<AppProvider>().removeMoviePoster(poster.id);
|
||||||
ToastUtil.show(context, '已删除');
|
Navigator.pop(context);
|
||||||
},
|
_loadPosters();
|
||||||
child: const Text('删除', style: TextStyle(color: Colors.red)),
|
ToastUtil.show(context, '已删除');
|
||||||
),
|
},
|
||||||
],
|
child: Text('删除', style: TextStyle(color: colors.error)),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('影评详情'),
|
title: const Text('影评详情'),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -70,9 +71,9 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
|||||||
// 影评内容
|
// 影评内容
|
||||||
Text(
|
Text(
|
||||||
_review.content,
|
_review.content,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.8,
|
height: 1.8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -82,7 +83,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
|||||||
// 分隔线
|
// 分隔线
|
||||||
Container(
|
Container(
|
||||||
height: 0.5,
|
height: 0.5,
|
||||||
color: const Color(0xFFE5E5E5),
|
color: colors.outline,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -92,6 +93,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
|||||||
icon: Icons.person_outline,
|
icon: Icons.person_outline,
|
||||||
label: '影评人:',
|
label: '影评人:',
|
||||||
value: _review.reviewer.isNotEmpty ? _review.reviewer : '匿名',
|
value: _review.reviewer.isNotEmpty ? _review.reviewer : '匿名',
|
||||||
|
colors: colors,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -102,6 +104,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
|||||||
icon: Icons.source_outlined,
|
icon: Icons.source_outlined,
|
||||||
label: '来源:',
|
label: '来源:',
|
||||||
value: _review.source,
|
value: _review.source,
|
||||||
|
colors: colors,
|
||||||
),
|
),
|
||||||
|
|
||||||
if (_review.source.isNotEmpty) const SizedBox(height: 16),
|
if (_review.source.isNotEmpty) const SizedBox(height: 16),
|
||||||
@@ -111,6 +114,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
|||||||
icon: Icons.category_outlined,
|
icon: Icons.category_outlined,
|
||||||
label: '类型:',
|
label: '类型:',
|
||||||
value: _review.typeText,
|
value: _review.typeText,
|
||||||
|
colors: colors,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -120,6 +124,7 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
|||||||
icon: Icons.access_time,
|
icon: Icons.access_time,
|
||||||
label: '时间:',
|
label: '时间:',
|
||||||
value: _formatDate(_review.createdAt),
|
value: _formatDate(_review.createdAt),
|
||||||
|
colors: colors,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -132,13 +137,14 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
|||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String label,
|
required String label,
|
||||||
required String value,
|
required String value,
|
||||||
|
required ColorScheme colors,
|
||||||
}) {
|
}) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: const Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
// 固定宽度容器,以"影评人:"的最大宽度为准
|
// 固定宽度容器,以"影评人:"的最大宽度为准
|
||||||
@@ -146,18 +152,18 @@ class _MovieReviewDetailPageState extends State<MovieReviewDetailPage> {
|
|||||||
width: 64,
|
width: 64,
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
value,
|
value,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -46,10 +46,11 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final isEdit = widget.review != null;
|
final isEdit = widget.review != null;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(isEdit ? '编辑影评' : '写影评'),
|
title: Text(isEdit ? '编辑影评' : '写影评'),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -73,24 +74,24 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
|||||||
// 顶部信息栏
|
// 顶部信息栏
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
|
bottom: BorderSide(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// 类型选择
|
// 类型选择
|
||||||
_buildTypeSelector(),
|
_buildTypeSelector(colors),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
// 评论人
|
// 评论人
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _reviewerController,
|
controller: _reviewerController,
|
||||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '评论人',
|
hintText: '评论人',
|
||||||
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
|
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
@@ -103,10 +104,10 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
|||||||
width: 100,
|
width: 100,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _sourceController,
|
controller: _sourceController,
|
||||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '来源',
|
hintText: '来源',
|
||||||
hintStyle: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
|
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
@@ -116,7 +117,7 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 评论内容区域
|
// 评论内容区域
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
@@ -124,19 +125,19 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
|||||||
maxLines: null,
|
maxLines: null,
|
||||||
expands: true,
|
expands: true,
|
||||||
textAlignVertical: TextAlignVertical.top,
|
textAlignVertical: TextAlignVertical.top,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.7,
|
height: 1.7,
|
||||||
),
|
),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '写下你的影评...',
|
hintText: '写下你的影评...',
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
),
|
),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.all(16),
|
contentPadding: const EdgeInsets.all(16),
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
@@ -153,29 +154,29 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 构建类型选择器
|
/// 构建类型选择器
|
||||||
Widget _buildTypeSelector() {
|
Widget _buildTypeSelector(ColorScheme colors) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => _showTypeSelector(),
|
onTap: () => _showTypeSelector(),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
border: Border.all(color: colors.outline),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_reviewType == 1 ? '短评' : '长评',
|
_reviewType == 1 ? '短评' : '长评',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.arrow_drop_down,
|
Icons.arrow_drop_down,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -187,36 +188,42 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
|||||||
void _showTypeSelector() {
|
void _showTypeSelector() {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.transparent,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
builder: (context) => SafeArea(
|
builder: (context) {
|
||||||
child: Column(
|
final colors = Theme.of(context).colorScheme;
|
||||||
mainAxisSize: MainAxisSize.min,
|
return Container(
|
||||||
children: [
|
color: colors.surface,
|
||||||
ListTile(
|
child: SafeArea(
|
||||||
title: const Text('短评'),
|
child: Column(
|
||||||
trailing: _reviewType == 1
|
mainAxisSize: MainAxisSize.min,
|
||||||
? const Icon(Icons.check, color: Color(0xFF1A1A1A))
|
children: [
|
||||||
: null,
|
ListTile(
|
||||||
onTap: () {
|
title: const Text('短评'),
|
||||||
setState(() => _reviewType = 1);
|
trailing: _reviewType == 1
|
||||||
Navigator.pop(context);
|
? Icon(Icons.check, color: colors.onSurface)
|
||||||
},
|
: null,
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _reviewType = 1);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Divider(height: 0.5, color: colors.outline),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('长评'),
|
||||||
|
trailing: _reviewType == 2
|
||||||
|
? Icon(Icons.check, color: colors.onSurface)
|
||||||
|
: null,
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _reviewType = 2);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5),
|
),
|
||||||
ListTile(
|
);
|
||||||
title: const Text('长评'),
|
},
|
||||||
trailing: _reviewType == 2
|
|
||||||
? const Icon(Icons.check, color: Color(0xFF1A1A1A))
|
|
||||||
: null,
|
|
||||||
onTap: () {
|
|
||||||
setState(() => _reviewType = 2);
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,19 +73,20 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: _isSearching
|
title: _isSearching
|
||||||
? TextField(
|
? TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '搜索影评内容、评论人、来源...',
|
hintText: '搜索影评内容、评论人、来源...',
|
||||||
hintStyle: TextStyle(color: Color(0xFF999999)),
|
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
style: const TextStyle(color: Color(0xFF1A1A1A)),
|
style: TextStyle(color: colors.onSurface),
|
||||||
onChanged: _onSearchChanged,
|
onChanged: _onSearchChanged,
|
||||||
)
|
)
|
||||||
: const Text('影评'),
|
: const Text('影评'),
|
||||||
@@ -112,6 +113,7 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState() {
|
Widget _buildEmptyState() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -120,21 +122,21 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
|||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.rate_review_outlined,
|
Icons.rate_review_outlined,
|
||||||
size: 40,
|
size: 40,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Text(
|
Text(
|
||||||
'暂无影评',
|
'暂无影评',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -143,15 +145,15 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'添加记录',
|
'添加记录',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Colors.white,
|
color: colors.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -176,13 +178,14 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildReviewCard(MovieReview review) {
|
Widget _buildReviewCard(MovieReview review) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => _navigateToReviewDetail(review),
|
onTap: () => _navigateToReviewDetail(review),
|
||||||
onLongPress: () => _showDeleteDialog(review),
|
onLongPress: () => _showDeleteDialog(review),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -193,8 +196,8 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: review.reviewType == 1
|
color: review.reviewType == 1
|
||||||
? Colors.white
|
? colors.surface
|
||||||
: const Color(0xFF1A1A1A),
|
: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -202,8 +205,8 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: review.reviewType == 1
|
color: review.reviewType == 1
|
||||||
? const Color(0xFF666666)
|
? colors.onSurface.withValues(alpha: 0.6)
|
||||||
: Colors.white,
|
: colors.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -215,9 +218,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
|||||||
review.content,
|
review.content,
|
||||||
maxLines: review.reviewType == 1 ? 4 : 8,
|
maxLines: review.reviewType == 1 ? 4 : 8,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -232,9 +235,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
review.reviewer,
|
review.reviewer,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -252,9 +255,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
review.source,
|
review.source,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -262,9 +265,9 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
|||||||
// 日期
|
// 日期
|
||||||
Text(
|
Text(
|
||||||
_formatDate(review.createdAt),
|
_formatDate(review.createdAt),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -315,28 +318,31 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
|||||||
void _showDeleteDialog(MovieReview review) {
|
void _showDeleteDialog(MovieReview review) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) {
|
||||||
backgroundColor: Colors.white,
|
final colors = Theme.of(context).colorScheme;
|
||||||
elevation: 0,
|
return AlertDialog(
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
backgroundColor: colors.surface,
|
||||||
title: const Text('确认删除'),
|
elevation: 0,
|
||||||
content: const Text('确定要删除这条影评吗?'),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
actions: [
|
title: const Text('确认删除'),
|
||||||
TextButton(
|
content: const Text('确定要删除这条影评吗?'),
|
||||||
onPressed: () => Navigator.pop(context),
|
actions: [
|
||||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
TextButton(
|
||||||
),
|
onPressed: () => Navigator.pop(context),
|
||||||
TextButton(
|
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
onPressed: () async {
|
),
|
||||||
await context.read<AppProvider>().removeMovieReview(review.id);
|
TextButton(
|
||||||
Navigator.pop(context);
|
onPressed: () async {
|
||||||
_loadReviews();
|
await context.read<AppProvider>().removeMovieReview(review.id);
|
||||||
ToastUtil.show(context, '已删除');
|
Navigator.pop(context);
|
||||||
},
|
_loadReviews();
|
||||||
child: const Text('删除', style: TextStyle(color: Colors.red)),
|
ToastUtil.show(context, '已删除');
|
||||||
),
|
},
|
||||||
],
|
child: Text('删除', style: TextStyle(color: colors.error)),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,21 +24,22 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: colors.surfaceContainerHighest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)),
|
icon: Icon(Icons.close, color: colors.onSurface),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'分享海报',
|
'分享海报',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
@@ -51,12 +52,12 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
|||||||
height: 20,
|
height: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Text(
|
: Text(
|
||||||
'分享',
|
'分享',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -77,13 +78,14 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
|||||||
|
|
||||||
/// 构建海报 Widget
|
/// 构建海报 Widget
|
||||||
Widget _buildPosterWidget() {
|
Widget _buildPosterWidget() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final movie = widget.movie;
|
final movie = widget.movie;
|
||||||
final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty;
|
final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 320,
|
width: 320,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -118,10 +120,10 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
|||||||
// 标题
|
// 标题
|
||||||
Text(
|
Text(
|
||||||
movie.title,
|
movie.title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -130,9 +132,9 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
movie.alternateTitles.join(' / '),
|
movie.alternateTitles.join(' / '),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -158,11 +160,11 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
const Text(
|
Text(
|
||||||
'/ 10',
|
'/ 10',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -172,44 +174,45 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
|||||||
|
|
||||||
// 导演
|
// 导演
|
||||||
if (movie.directors.isNotEmpty)
|
if (movie.directors.isNotEmpty)
|
||||||
_buildInfoRow('导演', movie.directors.join(' / ')),
|
_buildInfoRow('导演', movie.directors.join(' / '), colors),
|
||||||
|
|
||||||
// 编剧
|
// 编剧
|
||||||
if (movie.writers.isNotEmpty)
|
if (movie.writers.isNotEmpty)
|
||||||
_buildInfoRow('编剧', movie.writers.join(' / ')),
|
_buildInfoRow('编剧', movie.writers.join(' / '), colors),
|
||||||
|
|
||||||
// 主演
|
// 主演
|
||||||
if (movie.actors.isNotEmpty)
|
if (movie.actors.isNotEmpty)
|
||||||
_buildInfoRow('主演', movie.actors.take(3).join(' / ')),
|
_buildInfoRow('主演', movie.actors.take(3).join(' / '), colors),
|
||||||
|
|
||||||
// 类型
|
// 类型
|
||||||
if (movie.genres.isNotEmpty)
|
if (movie.genres.isNotEmpty)
|
||||||
_buildInfoRow('类型', movie.genres.join(' / ')),
|
_buildInfoRow('类型', movie.genres.join(' / '), colors),
|
||||||
|
|
||||||
// 上映日期
|
// 上映日期
|
||||||
if (movie.releaseDate != null)
|
if (movie.releaseDate != null)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
'上映',
|
'上映',
|
||||||
'${movie.releaseDate!.year}.${movie.releaseDate!.month.toString().padLeft(2, '0')}.${movie.releaseDate!.day.toString().padLeft(2, '0')}',
|
'${movie.releaseDate!.year}.${movie.releaseDate!.month.toString().padLeft(2, '0')}.${movie.releaseDate!.day.toString().padLeft(2, '0')}',
|
||||||
|
colors,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 简介
|
// 简介
|
||||||
if (movie.summary != null && movie.summary!.isNotEmpty) ...[
|
if (movie.summary != null && movie.summary!.isNotEmpty) ...[
|
||||||
const Text(
|
Text(
|
||||||
'简介',
|
'简介',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
movie.summary!,
|
movie.summary!,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
height: 1.6,
|
height: 1.6,
|
||||||
),
|
),
|
||||||
maxLines: 5,
|
maxLines: 5,
|
||||||
@@ -220,7 +223,7 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// 底部标识
|
// 底部标识
|
||||||
const Divider(height: 1, color: Color(0xFFE8E8E8)),
|
Divider(height: 1, color: colors.outline),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -228,14 +231,14 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.movie_outlined,
|
Icons.movie_outlined,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: const Color(0xFF1A1A1A).withOpacity(0.5),
|
color: colors.onSurface.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'来自 MookNote',
|
'来自 MookNote',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: const Color(0xFF1A1A1A).withOpacity(0.5),
|
color: colors.onSurface.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -249,7 +252,7 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 构建信息行
|
/// 构建信息行
|
||||||
Widget _buildInfoRow(String label, String value) {
|
Widget _buildInfoRow(String label, String value, ColorScheme colors) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -257,17 +260,17 @@ class _MovieSharePageState extends State<MovieSharePage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'$label:',
|
'$label:',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
value,
|
value,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF333333),
|
color: colors.onSurface.withValues(alpha: 0.75),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class MovieTabPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MovieTabPageState extends State<MovieTabPage> {
|
class _MovieTabPageState extends State<MovieTabPage> {
|
||||||
int _layoutStyle = 0; // 0: 海报网格, 1: 列表
|
int _layoutStyle = 0;
|
||||||
bool _firstLoad = true;
|
bool _firstLoad = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -31,10 +31,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
const MovieStatusBar(),
|
const MovieStatusBar(),
|
||||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildMovieList(context),
|
child: _buildMovieList(context),
|
||||||
),
|
),
|
||||||
@@ -43,12 +44,12 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMovieList(BuildContext context) {
|
Widget _buildMovieList(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
final statusMap = {0: 'watched', 1: 'watching', 2: 'want_to_watch'};
|
final statusMap = {0: 'watched', 1: 'watching', 2: 'want_to_watch'};
|
||||||
final currentStatus = statusMap[provider.movieStatusIndex]!;
|
final currentStatus = statusMap[provider.movieStatusIndex]!;
|
||||||
final allMovies = provider.movies.where((m) => !m.isDeleted).toList();
|
final allMovies = provider.movies.where((m) => !m.isDeleted).toList();
|
||||||
// 首次加载且数据为空时才显示骨架屏
|
|
||||||
if (_firstLoad && allMovies.isEmpty) {
|
if (_firstLoad && allMovies.isEmpty) {
|
||||||
return _buildSkeleton();
|
return _buildSkeleton();
|
||||||
}
|
}
|
||||||
@@ -59,8 +60,8 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
if (movies.isEmpty) {
|
if (movies.isEmpty) {
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async => await provider.loadMovies(),
|
onRefresh: () async => await provider.loadMovies(),
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
children: [_buildEmptyState(context, provider.movieStatusIndex)],
|
children: [_buildEmptyState(context, provider.movieStatusIndex)],
|
||||||
@@ -77,10 +78,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildGridView(List movies, AppProvider provider) {
|
Widget _buildGridView(List movies, AppProvider provider) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async => await provider.loadMovies(),
|
onRefresh: () async => await provider.loadMovies(),
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 100),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 100),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
@@ -96,10 +98,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildListView(List movies, AppProvider provider) {
|
Widget _buildListView(List movies, AppProvider provider) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async => await provider.loadMovies(),
|
onRefresh: () async => await provider.loadMovies(),
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
||||||
itemCount: movies.length,
|
itemCount: movies.length,
|
||||||
@@ -109,6 +112,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildListCard(movie) {
|
Widget _buildListCard(movie) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: movie),
|
onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: movie),
|
||||||
onLongPress: () => _showDeleteDialog(context, movie),
|
onLongPress: () => _showDeleteDialog(context, movie),
|
||||||
@@ -116,23 +120,22 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// 海报缩略图
|
|
||||||
Container(
|
Container(
|
||||||
width: 48, height: 64,
|
width: 48, height: 64,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF0F0F0),
|
color: colors.outlineVariant,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: movie.posterPath != null && movie.posterPath!.isNotEmpty
|
child: movie.posterPath != null && movie.posterPath!.isNotEmpty
|
||||||
? Image.file(File(movie.posterPath!), fit: BoxFit.cover,
|
? Image.file(File(movie.posterPath!), fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) => const Icon(Icons.movie_outlined, size: 22, color: Color(0xFFCCCCCC)))
|
errorBuilder: (_, __, ___) => Icon(Icons.movie_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)))
|
||||||
: const Icon(Icons.movie_outlined, size: 22, color: Color(0xFFCCCCCC)),
|
: Icon(Icons.movie_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -140,11 +143,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(movie.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
Text(movie.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
if (movie.alternateTitles.isNotEmpty) ...[
|
if (movie.alternateTitles.isNotEmpty) ...[
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text(movie.alternateTitles.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis,
|
Text(movie.alternateTitles.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
if (movie.rating != null)
|
if (movie.rating != null)
|
||||||
@@ -155,7 +158,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
|
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -163,19 +166,20 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showDeleteDialog(BuildContext context, movie) {
|
void _showDeleteDialog(BuildContext context, movie) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: const Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
content: Text('确定要删除《${movie.title}》吗?删除后可在回收站恢复。',
|
content: Text('确定要删除《${movie.title}》吗?删除后可在回收站恢复。',
|
||||||
style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)),
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx),
|
onPressed: () => Navigator.pop(ctx),
|
||||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
|
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -183,7 +187,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red, foregroundColor: Colors.white, elevation: 0,
|
backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
@@ -200,6 +204,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildListSkeleton() {
|
Widget _buildListSkeleton() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
||||||
itemCount: 6,
|
itemCount: 6,
|
||||||
@@ -207,7 +212,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: const Row(
|
child: const Row(
|
||||||
@@ -235,6 +240,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState(BuildContext context, int statusIndex) {
|
Widget _buildEmptyState(BuildContext context, int statusIndex) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final statusText = ['已看', '在看', '想看'][statusIndex];
|
final statusText = ['已看', '在看', '想看'][statusIndex];
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -242,11 +248,11 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 80, height: 80,
|
width: 80, height: 80,
|
||||||
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(20)),
|
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)),
|
||||||
child: const Icon(Icons.movie_outlined, size: 40, color: Color(0xFFCCCCCC)),
|
child: Icon(Icons.movie_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text('暂无$statusText的影片', style: const TextStyle(fontSize: 16, color: Color(0xFF999999))),
|
Text('暂无$statusText的影片', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -255,8 +261,8 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(8)),
|
decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(8)),
|
||||||
child: const Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
child: Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -22,13 +22,14 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final note = context.watch<AppProvider>().notes.firstWhere(
|
final note = context.watch<AppProvider>().notes.firstWhere(
|
||||||
(n) => n.id == widget.note.id,
|
(n) => n.id == widget.note.id,
|
||||||
orElse: () => widget.note,
|
orElse: () => widget.note,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
note.title.isNotEmpty
|
note.title.isNotEmpty
|
||||||
@@ -42,22 +43,21 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
// 日期 + 字数
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
|
bottom: BorderSide(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${note.createdAt.day}',
|
'${note.createdAt.day}',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
fontWeight: FontWeight.w200,
|
fontWeight: FontWeight.w200,
|
||||||
color: Color(0xFF333333),
|
color: colors.onSurface.withValues(alpha: 0.75),
|
||||||
height: 1.0,
|
height: 1.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -68,48 +68,44 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${note.createdAt.year}/${note.createdAt.month.toString().padLeft(2, '0')} 周${_weekdays[note.createdAt.weekday - 1]}',
|
'${note.createdAt.year}/${note.createdAt.month.toString().padLeft(2, '0')} 周${_weekdays[note.createdAt.weekday - 1]}',
|
||||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: Color(0xFF777777)),
|
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.55)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 1),
|
const SizedBox(height: 1),
|
||||||
Text(
|
Text(
|
||||||
'${note.createdAt.hour.toString().padLeft(2, '0')}:${note.createdAt.minute.toString().padLeft(2, '0')}',
|
'${note.createdAt.hour.toString().padLeft(2, '0')}:${note.createdAt.minute.toString().padLeft(2, '0')}',
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
|
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
'${note.content.length} 字',
|
'${note.content.length} 字',
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)),
|
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 标签行
|
|
||||||
if (note.tags.isNotEmpty)
|
if (note.tags.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 6),
|
padding: const EdgeInsets.only(top: 6),
|
||||||
child: _buildTagRow(note.tags),
|
child: _buildTagRow(note.tags),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 内容
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Markdown(
|
child: Markdown(
|
||||||
data: note.content,
|
data: note.content,
|
||||||
styleSheet: _buildMarkdownStyleSheet(),
|
styleSheet: _buildMarkdownStyleSheet(colors),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
// ignore: deprecated_member_use
|
// ignore: deprecated_member_use
|
||||||
imageBuilder: (uri, title, alt) => _buildMarkdownImage(uri, note),
|
imageBuilder: (uri, title, alt) => _buildMarkdownImage(uri, note),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 图片行
|
|
||||||
if (note.images.isNotEmpty) _buildImageRow(note.images),
|
if (note.images.isNotEmpty) _buildImageRow(note.images),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// 右下角悬浮按钮组
|
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 16,
|
right: 16,
|
||||||
bottom: 24,
|
bottom: 24,
|
||||||
@@ -121,6 +117,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTagRow(List<String> tags) {
|
Widget _buildTagRow(List<String> tags) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
height: 28,
|
height: 28,
|
||||||
margin: const EdgeInsets.only(bottom: 2),
|
margin: const EdgeInsets.only(bottom: 2),
|
||||||
@@ -133,12 +130,12 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
tags[index],
|
tags[index],
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF666666)),
|
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -147,12 +144,13 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImageRow(List<String> images) {
|
Widget _buildImageRow(List<String> images) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
height: 80,
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
border: const Border(
|
border: Border(
|
||||||
top: BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
|
top: BorderSide(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -175,15 +173,15 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
height: 64,
|
height: 64,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Image.file(
|
child: Image.file(
|
||||||
File(images[index]),
|
File(images[index]),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) => Container(
|
errorBuilder: (_, __, ___) => Container(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
child: const Icon(Icons.broken_image_outlined, size: 20, color: Color(0xFFCCCCCC)),
|
child: Icon(Icons.broken_image_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -193,37 +191,37 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkdownStyleSheet _buildMarkdownStyleSheet() {
|
MarkdownStyleSheet _buildMarkdownStyleSheet(ColorScheme colors) {
|
||||||
return MarkdownStyleSheet(
|
return MarkdownStyleSheet(
|
||||||
h1: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4),
|
h1: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4),
|
||||||
h2: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4),
|
h2: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4),
|
||||||
h3: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4),
|
h3: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4),
|
||||||
h4: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4),
|
h4: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4),
|
||||||
p: const TextStyle(fontSize: 15, color: Color(0xFF333333), height: 1.8),
|
p: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.75), height: 1.8),
|
||||||
code: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A), backgroundColor: Color(0xFFF5F5F5), fontFamily: 'monospace'),
|
code: TextStyle(fontSize: 14, color: colors.onSurface, backgroundColor: colors.surfaceContainerHighest, fontFamily: 'monospace'),
|
||||||
codeblockDecoration: BoxDecoration(
|
codeblockDecoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHigh,
|
||||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
border: Border.all(color: colors.outline),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
codeblockPadding: const EdgeInsets.all(12),
|
codeblockPadding: const EdgeInsets.all(12),
|
||||||
blockquote: const TextStyle(fontSize: 15, color: Color(0xFF666666), fontStyle: FontStyle.italic, height: 1.8),
|
blockquote: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.6), fontStyle: FontStyle.italic, height: 1.8),
|
||||||
blockquoteDecoration: const BoxDecoration(
|
blockquoteDecoration: BoxDecoration(
|
||||||
border: Border(left: BorderSide(color: Color(0xFF999999), width: 4)),
|
border: Border(left: BorderSide(color: colors.onSurface.withValues(alpha: 0.4), width: 4)),
|
||||||
),
|
),
|
||||||
blockquotePadding: const EdgeInsets.only(left: 12, top: 4, bottom: 4),
|
blockquotePadding: const EdgeInsets.only(left: 12, top: 4, bottom: 4),
|
||||||
listBullet: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
|
listBullet: TextStyle(fontSize: 15, color: colors.onSurface),
|
||||||
listIndent: 24,
|
listIndent: 24,
|
||||||
a: const TextStyle(fontSize: 15, color: Color(0xFF4A90D9), decoration: TextDecoration.underline),
|
a: const TextStyle(fontSize: 15, color: Color(0xFF4A90D9), decoration: TextDecoration.underline),
|
||||||
tableHead: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
tableHead: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||||
tableBody: const TextStyle(fontSize: 14, color: Color(0xFF333333)),
|
tableBody: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75)),
|
||||||
tableBorder: TableBorder.all(color: const Color(0xFFE5E5E5), width: 0.5),
|
tableBorder: TableBorder.all(color: colors.outline, width: 0.5),
|
||||||
tableColumnWidth: const FlexColumnWidth(),
|
tableColumnWidth: const FlexColumnWidth(),
|
||||||
tableCellsDecoration: const BoxDecoration(color: Colors.white),
|
tableCellsDecoration: BoxDecoration(color: colors.surface),
|
||||||
tablePadding: const EdgeInsets.all(8),
|
tablePadding: const EdgeInsets.all(8),
|
||||||
strong: const TextStyle(fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
strong: TextStyle(fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||||
em: const TextStyle(fontStyle: FontStyle.italic, color: Color(0xFF333333)),
|
em: TextStyle(fontStyle: FontStyle.italic, color: colors.onSurface.withValues(alpha: 0.75)),
|
||||||
del: const TextStyle(decoration: TextDecoration.lineThrough, color: Color(0xFF999999)),
|
del: TextStyle(decoration: TextDecoration.lineThrough, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +270,6 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 截取内容前N个字作为标题
|
|
||||||
String _truncateContent(String content) {
|
String _truncateContent(String content) {
|
||||||
final cleaned = content
|
final cleaned = content
|
||||||
.replaceAll(RegExp(r'^#+\s+', multiLine: true), '')
|
.replaceAll(RegExp(r'^#+\s+', multiLine: true), '')
|
||||||
@@ -298,8 +295,8 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 右下角悬浮按钮组
|
|
||||||
Widget _buildFloatingActionButtons() {
|
Widget _buildFloatingActionButtons() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -307,13 +304,16 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
icon: Icons.edit_outlined,
|
icon: Icons.edit_outlined,
|
||||||
onPressed: () => _navigateToEdit(context),
|
onPressed: () => _navigateToEdit(context),
|
||||||
tooltip: '编辑',
|
tooltip: '编辑',
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
foregroundColor: colors.onPrimary,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildFloatingButton(
|
_buildFloatingButton(
|
||||||
icon: Icons.delete_outline,
|
icon: Icons.delete_outline,
|
||||||
onPressed: () => _showDeleteDialog(context),
|
onPressed: () => _showDeleteDialog(context),
|
||||||
tooltip: '删除',
|
tooltip: '删除',
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: colors.error,
|
||||||
|
foregroundColor: colors.onError,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildFloatingButton(
|
_buildFloatingButton(
|
||||||
@@ -321,6 +321,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
onPressed: _shareNote,
|
onPressed: _shareNote,
|
||||||
tooltip: '分享',
|
tooltip: '分享',
|
||||||
backgroundColor: const Color(0xFF4CAF50),
|
backgroundColor: const Color(0xFF4CAF50),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -330,7 +331,8 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
required IconData icon,
|
required IconData icon,
|
||||||
required VoidCallback onPressed,
|
required VoidCallback onPressed,
|
||||||
required String tooltip,
|
required String tooltip,
|
||||||
Color backgroundColor = const Color(0xFF1A1A1A),
|
required Color backgroundColor,
|
||||||
|
required Color foregroundColor,
|
||||||
}) {
|
}) {
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
@@ -349,7 +351,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(icon, size: 18, color: Colors.white),
|
icon: Icon(icon, size: 18, color: foregroundColor),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
tooltip: tooltip,
|
tooltip: tooltip,
|
||||||
@@ -375,7 +377,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
|||||||
context.read<AppProvider>().removeNote(widget.note.id);
|
context.read<AppProvider>().removeNote(widget.note.id);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: const Text('删除', style: TextStyle(color: Colors.red)),
|
child: Text('删除', style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -59,8 +59,9 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: GestureDetector(
|
title: GestureDetector(
|
||||||
@@ -73,8 +74,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: _saveNote,
|
onPressed: _saveNote,
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF1A1A1A),
|
backgroundColor: colors.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colors.onPrimary,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
@@ -96,9 +97,9 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
// 顶部信息栏
|
// 顶部信息栏
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
|
bottom: BorderSide(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -110,10 +111,10 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
// 大日号
|
// 大日号
|
||||||
Text(
|
Text(
|
||||||
'${_createdAt.day}',
|
'${_createdAt.day}',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
fontWeight: FontWeight.w200,
|
fontWeight: FontWeight.w200,
|
||||||
color: Color(0xFF333333),
|
color: colors.onSurface.withValues(alpha: 0.75),
|
||||||
height: 1.0,
|
height: 1.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -125,18 +126,18 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${_createdAt.year}/${_createdAt.month.toString().padLeft(2, '0')} 周${_weekdays[_createdAt.weekday - 1]}',
|
'${_createdAt.year}/${_createdAt.month.toString().padLeft(2, '0')} 周${_weekdays[_createdAt.weekday - 1]}',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF777777),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 1),
|
const SizedBox(height: 1),
|
||||||
Text(
|
Text(
|
||||||
'${_createdAt.hour.toString().padLeft(2, '0')}:${_createdAt.minute.toString().padLeft(2, '0')}',
|
'${_createdAt.hour.toString().padLeft(2, '0')}:${_createdAt.minute.toString().padLeft(2, '0')}',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -145,9 +146,9 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
// 字数统计
|
// 字数统计
|
||||||
Text(
|
Text(
|
||||||
'$_charCount 字',
|
'$_charCount 字',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Color(0xFFAAAAAA),
|
color: colors.onSurface.withValues(alpha: 0.35),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -208,13 +209,14 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
|
|
||||||
/// 现代极简底部工具栏
|
/// 现代极简底部工具栏
|
||||||
Widget _buildFloatingToolbar() {
|
Widget _buildFloatingToolbar() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.fromLTRB(12, 8, 12, 10),
|
margin: const EdgeInsets.fromLTRB(12, 8, 12, 10),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: const Color(0xFFEAEAEA), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.04),
|
color: Colors.black.withValues(alpha: 0.04),
|
||||||
@@ -251,7 +253,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
// 视图模式切换
|
// 视图模式切换
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Container(
|
Container(
|
||||||
width: 1, height: 20, color: const Color(0xFFE5E5E5),
|
width: 1, height: 20, color: colors.outline,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_modeSwitch(Icons.edit, 'edit'),
|
_modeSwitch(Icons.edit, 'edit'),
|
||||||
@@ -262,6 +264,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _toolBtn(IconData icon, String tooltip, VoidCallback onTap) {
|
Widget _toolBtn(IconData icon, String tooltip, VoidCallback onTap) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@@ -271,7 +274,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
message: tooltip,
|
message: tooltip,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 6),
|
||||||
child: Icon(icon, size: 21, color: const Color(0xFF555555)),
|
child: Icon(icon, size: 21, color: colors.onSurface.withValues(alpha: 0.7)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -279,16 +282,18 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _toolGap() {
|
Widget _toolGap() {
|
||||||
return const Padding(
|
final colors = Theme.of(context).colorScheme;
|
||||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 18,
|
height: 18,
|
||||||
child: VerticalDivider(width: 0, thickness: 0.5, color: Color(0xFFE0E0E0)),
|
child: VerticalDivider(width: 0, thickness: 0.5, color: colors.outline),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _modeSwitch(IconData icon, String mode) {
|
Widget _modeSwitch(IconData icon, String mode) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final active = _editorMode == mode;
|
final active = _editorMode == mode;
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
@@ -298,13 +303,13 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: active ? const Color(0xFF1A1A1A) : Colors.transparent,
|
color: active ? colors.primary : Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: active ? Colors.white : const Color(0xFFAAAAAA),
|
color: active ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.35),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -374,6 +379,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
|
|
||||||
/// 编辑器
|
/// 编辑器
|
||||||
Widget _buildEditor() {
|
Widget _buildEditor() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: _contentController,
|
controller: _contentController,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
@@ -384,40 +390,41 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
height: 1.6,
|
height: 1.6,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.6,
|
height: 1.6,
|
||||||
),
|
),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '使用 Markdown 格式书写...',
|
hintText: '使用 Markdown 格式书写...',
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
height: 1.6,
|
height: 1.6,
|
||||||
),
|
),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
focusedBorder: InputBorder.none,
|
focusedBorder: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.all(16),
|
contentPadding: const EdgeInsets.all(16),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 实时 Markdown 预览(点击回到编辑)
|
/// 实时 Markdown 预览(点击回到编辑)
|
||||||
Widget _buildPreview() {
|
Widget _buildPreview() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final text = _contentController.text;
|
final text = _contentController.text;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => setState(() => _editorMode = 'edit'),
|
onTap: () => setState(() => _editorMode = 'edit'),
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
child: text.isEmpty
|
child: text.isEmpty
|
||||||
? const Center(
|
? Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'预览区域',
|
'预览区域',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -426,45 +433,45 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
selectable: true,
|
selectable: true,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
styleSheet: MarkdownStyleSheet(
|
styleSheet: MarkdownStyleSheet(
|
||||||
h1: const TextStyle(
|
h1: TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
h2: const TextStyle(
|
h2: TextStyle(
|
||||||
fontSize: 19,
|
fontSize: 19,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
h3: const TextStyle(
|
h3: TextStyle(
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
p: const TextStyle(
|
p: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF333333),
|
color: colors.onSurface.withValues(alpha: 0.75),
|
||||||
height: 1.7,
|
height: 1.7,
|
||||||
),
|
),
|
||||||
code: const TextStyle(
|
code: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
backgroundColor: Color(0xFFF0F0F0),
|
backgroundColor: colors.outlineVariant,
|
||||||
),
|
),
|
||||||
codeblockDecoration: BoxDecoration(
|
codeblockDecoration: BoxDecoration(
|
||||||
color: const Color(0xFFF0F0F0),
|
color: colors.outlineVariant,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
blockquote: const TextStyle(
|
blockquote: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
blockquoteDecoration: const BoxDecoration(
|
blockquoteDecoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
left: BorderSide(color: Color(0xFFCCCCCC), width: 3),
|
left: BorderSide(color: colors.onSurface.withValues(alpha: 0.25), width: 3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -494,10 +501,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTagChip(int index) {
|
Widget _buildTagChip(int index) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -505,12 +513,12 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_tags[index],
|
_tags[index],
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF666666)),
|
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 3),
|
const SizedBox(width: 3),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => setState(() => _tags.removeAt(index)),
|
onTap: () => setState(() => _tags.removeAt(index)),
|
||||||
child: const Icon(Icons.close, size: 10, color: Color(0xFFBBBBBB)),
|
child: Icon(Icons.close, size: 10, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -518,20 +526,21 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAddTagButton() {
|
Widget _buildAddTagButton() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: _showAddTagDialog,
|
onTap: _showAddTagDialog,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
border: Border.all(color: const Color(0xFFDDDDDD), width: 1),
|
border: Border.all(color: colors.onSurface.withValues(alpha: 0.25), width: 1),
|
||||||
),
|
),
|
||||||
child: const Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.add, size: 12, color: Color(0xFFAAAAAA)),
|
Icon(Icons.add, size: 12, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||||
SizedBox(width: 2),
|
const SizedBox(width: 2),
|
||||||
Text('标签', style: TextStyle(fontSize: 11, color: Color(0xFFAAAAAA))),
|
Text('标签', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -550,18 +559,20 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => StatefulBuilder(
|
builder: (ctx) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return StatefulBuilder(
|
||||||
builder: (ctx, setDialogState) => AlertDialog(
|
builder: (ctx, setDialogState) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'添加标签',
|
'添加标签',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
||||||
@@ -575,13 +586,13 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||||
cursorColor: const Color(0xFF1A1A1A),
|
cursorColor: colors.primary,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '输入新标签名称',
|
hintText: '输入新标签名称',
|
||||||
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFBBBBBB)),
|
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFFF8F8F8),
|
fillColor: colors.surfaceContainerHigh,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
@@ -593,11 +604,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1),
|
borderSide: BorderSide(color: colors.primary, width: 1),
|
||||||
),
|
),
|
||||||
suffixIcon: controller.text.isNotEmpty
|
suffixIcon: controller.text.isNotEmpty
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: const Icon(Icons.clear, size: 16, color: Color(0xFFAAAAAA)),
|
icon: Icon(Icons.clear, size: 16, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||||
onPressed: () => controller.clear(),
|
onPressed: () => controller.clear(),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
@@ -613,11 +624,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
// 已有标签列表
|
// 已有标签列表
|
||||||
if (availableTags.isNotEmpty) ...[
|
if (availableTags.isNotEmpty) ...[
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Text(
|
Text(
|
||||||
'或选择已有标签',
|
'或选择已有标签',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Color(0xFFAAAAAA),
|
color: colors.onSurface.withValues(alpha: 0.35),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -637,15 +648,15 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
tag,
|
tag,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF555555),
|
color: colors.onSurface.withValues(alpha: 0.7),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -673,7 +684,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx),
|
onPressed: () => Navigator.pop(ctx),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFF999999),
|
foregroundColor: colors.onSurface.withValues(alpha: 0.4),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
child: const Text('取消', style: TextStyle(fontSize: 14)),
|
child: const Text('取消', style: TextStyle(fontSize: 14)),
|
||||||
@@ -684,8 +695,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF1A1A1A),
|
backgroundColor: colors.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colors.onPrimary,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||||
@@ -695,10 +706,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
],
|
],
|
||||||
actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取所有已有标签(从所有笔记中收集)
|
/// 获取所有已有标签(从所有笔记中收集)
|
||||||
List<String> _getAllExistingTags(AppProvider provider) {
|
List<String> _getAllExistingTags(AppProvider provider) {
|
||||||
final allTags = <String>{};
|
final allTags = <String>{};
|
||||||
@@ -717,6 +729,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAppBarTitle() {
|
Widget _buildAppBarTitle() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final base = _isEditing ? '编辑笔记' : '新建笔记';
|
final base = _isEditing ? '编辑笔记' : '新建笔记';
|
||||||
final t = _titleController.text.trim();
|
final t = _titleController.text.trim();
|
||||||
if (t.isEmpty) {
|
if (t.isEmpty) {
|
||||||
@@ -725,7 +738,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(base),
|
Text(base),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
const Icon(Icons.edit, size: 14, color: Color(0xFF999999)),
|
Icon(Icons.edit, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -738,8 +751,10 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
final controller = TextEditingController(text: _titleController.text);
|
final controller = TextEditingController(text: _titleController.text);
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) {
|
||||||
backgroundColor: Colors.white,
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
@@ -749,23 +764,23 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
style: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A)),
|
style: TextStyle(fontSize: 16, color: colors.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '输入标题...',
|
hintText: '输入标题...',
|
||||||
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)),
|
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFFFAFAFA),
|
fillColor: colors.surfaceContainerHigh,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
|
borderSide: BorderSide(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE8E8E8), width: 0.5),
|
borderSide: BorderSide(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1),
|
borderSide: BorderSide(color: colors.primary, width: 1),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
),
|
),
|
||||||
@@ -778,7 +793,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFF666666),
|
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
child: const Text('取消'),
|
child: const Text('取消'),
|
||||||
),
|
),
|
||||||
@@ -788,8 +803,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF1A1A1A),
|
backgroundColor: colors.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colors.onPrimary,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
),
|
),
|
||||||
@@ -797,7 +812,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,7 +842,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
} else {
|
} else {
|
||||||
// 添加新笔记 - 先创建笔记获取ID
|
// 添加新笔记 - 先创建笔记获取ID
|
||||||
final noteId = now.millisecondsSinceEpoch.toString();
|
final noteId = now.millisecondsSinceEpoch.toString();
|
||||||
|
|
||||||
// 如果有图片,需要移动到正确的ID目录
|
// 如果有图片,需要移动到正确的ID目录
|
||||||
List<String> finalImages = [];
|
List<String> finalImages = [];
|
||||||
if (_images.isNotEmpty) {
|
if (_images.isNotEmpty) {
|
||||||
@@ -835,7 +851,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
final newNoteId = noteId;
|
final newNoteId = noteId;
|
||||||
finalImages = await _moveImagesToNewId(oldNoteId, newNoteId);
|
finalImages = await _moveImagesToNewId(oldNoteId, newNoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
final title = _titleController.text.trim();
|
final title = _titleController.text.trim();
|
||||||
|
|
||||||
final newNote = Note(
|
final newNote = Note(
|
||||||
@@ -856,17 +872,17 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
|
|
||||||
// 刷新笔记列表
|
// 刷新笔记列表
|
||||||
await context.read<AppProvider>().loadNotes();
|
await context.read<AppProvider>().loadNotes();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将图片从临时ID目录移动到新ID目录
|
/// 将图片从临时ID目录移动到新ID目录
|
||||||
Future<List<String>> _moveImagesToNewId(String oldNoteId, String newNoteId) async {
|
Future<List<String>> _moveImagesToNewId(String oldNoteId, String newNoteId) async {
|
||||||
final List<String> newPaths = [];
|
final List<String> newPaths = [];
|
||||||
|
|
||||||
final newDir = await ImagePathHelper.instance.getNoteImagesDir(newNoteId);
|
final newDir = await ImagePathHelper.instance.getNoteImagesDir(newNoteId);
|
||||||
|
|
||||||
for (final imagePath in _images) {
|
for (final imagePath in _images) {
|
||||||
// 使用路径分隔符检查,兼容 Windows 和 Unix
|
// 使用路径分隔符检查,兼容 Windows 和 Unix
|
||||||
final normalizedPath = imagePath.replaceAll('\\', '/');
|
final normalizedPath = imagePath.replaceAll('\\', '/');
|
||||||
@@ -874,9 +890,9 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
// 需要移动的文件
|
// 需要移动的文件
|
||||||
final fileName = p.basename(imagePath);
|
final fileName = p.basename(imagePath);
|
||||||
final newPath = p.join(newDir, fileName);
|
final newPath = p.join(newDir, fileName);
|
||||||
|
|
||||||
await ImagePathHelper.instance.ensureDirExists(newDir);
|
await ImagePathHelper.instance.ensureDirExists(newDir);
|
||||||
|
|
||||||
// 检查源文件是否存在
|
// 检查源文件是否存在
|
||||||
final sourceFile = File(imagePath);
|
final sourceFile = File(imagePath);
|
||||||
if (await sourceFile.exists()) {
|
if (await sourceFile.exists()) {
|
||||||
@@ -888,14 +904,14 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
newPaths.add(imagePath);
|
newPaths.add(imagePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除旧目录
|
// 删除旧目录
|
||||||
try {
|
try {
|
||||||
await ImagePathHelper.instance.deleteNoteImages(oldNoteId);
|
await ImagePathHelper.instance.deleteNoteImages(oldNoteId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 忽略删除失败
|
// 忽略删除失败
|
||||||
}
|
}
|
||||||
|
|
||||||
return newPaths;
|
return newPaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,11 +924,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
maxHeight: 1920,
|
maxHeight: 1920,
|
||||||
imageQuality: 85,
|
imageQuality: 85,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
// 生成唯一的文件名
|
// 生成唯一的文件名
|
||||||
final fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg';
|
final fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||||
|
|
||||||
// 如果是编辑模式,使用现有笔记ID;如果是新建模式,使用临时ID(保存时会替换)
|
// 如果是编辑模式,使用现有笔记ID;如果是新建模式,使用临时ID(保存时会替换)
|
||||||
String noteId;
|
String noteId;
|
||||||
if (_isEditing) {
|
if (_isEditing) {
|
||||||
@@ -922,14 +938,14 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
noteId = _tempNoteId ?? DateTime.now().millisecondsSinceEpoch.toString();
|
noteId = _tempNoteId ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
_tempNoteId = noteId;
|
_tempNoteId = noteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制图片到应用目录: images/notes/{noteId}/{fileName}
|
// 复制图片到应用目录: images/notes/{noteId}/{fileName}
|
||||||
final targetDir = await ImagePathHelper.instance.getNoteImagesDir(noteId);
|
final targetDir = await ImagePathHelper.instance.getNoteImagesDir(noteId);
|
||||||
await ImagePathHelper.instance.ensureDirExists(targetDir);
|
await ImagePathHelper.instance.ensureDirExists(targetDir);
|
||||||
final targetPath = p.join(targetDir, fileName);
|
final targetPath = p.join(targetDir, fileName);
|
||||||
|
|
||||||
await File(image.path).copy(targetPath);
|
await File(image.path).copy(targetPath);
|
||||||
|
|
||||||
setState(() => _images.add(targetPath));
|
setState(() => _images.add(targetPath));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -939,11 +955,12 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
|
|
||||||
/// 构建图片横向滚动行
|
/// 构建图片横向滚动行
|
||||||
Widget _buildImageRow() {
|
Widget _buildImageRow() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
if (_images.isEmpty) {
|
if (_images.isEmpty) {
|
||||||
return Container(
|
return Container(
|
||||||
height: 80,
|
height: 80,
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
),
|
),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
@@ -958,7 +975,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
return Container(
|
return Container(
|
||||||
height: 88,
|
height: 88,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.02),
|
color: Colors.black.withValues(alpha: 0.02),
|
||||||
@@ -984,6 +1001,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
|
|
||||||
/// 添加图片按钮
|
/// 添加图片按钮
|
||||||
Widget _buildAddImageButton() {
|
Widget _buildAddImageButton() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: _pickImage,
|
onTap: _pickImage,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -992,13 +1010,13 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
height: 64,
|
height: 64,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHigh,
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.add_photo_alternate_outlined,
|
Icons.add_photo_alternate_outlined,
|
||||||
size: 24,
|
size: 24,
|
||||||
color: Color(0xFFBBBBBB),
|
color: colors.onSurface.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1006,6 +1024,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
|
|
||||||
/// 构建图片项
|
/// 构建图片项
|
||||||
Widget _buildImageItem(int index) {
|
Widget _buildImageItem(int index) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => _showImagePreview(index),
|
onTap: () => _showImagePreview(index),
|
||||||
onLongPress: () => _showDeleteImageDialog(index),
|
onLongPress: () => _showDeleteImageDialog(index),
|
||||||
@@ -1015,7 +1034,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
height: 64,
|
height: 64,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Image.file(
|
child: Image.file(
|
||||||
@@ -1056,8 +1075,10 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
void _showDeleteImageDialog(int index) {
|
void _showDeleteImageDialog(int index) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) {
|
||||||
backgroundColor: Colors.white,
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
@@ -1067,11 +1088,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
content: const Text(
|
content: Text(
|
||||||
'确定要删除这张图片吗?此操作不可恢复。',
|
'确定要删除这张图片吗?此操作不可恢复。',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1079,7 +1100,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFF666666),
|
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
child: const Text('取消'),
|
child: const Text('取消'),
|
||||||
@@ -1090,8 +1111,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: colors.error,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colors.onError,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -1102,7 +1123,8 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,21 +26,22 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: colors.surfaceContainerHighest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)),
|
icon: Icon(Icons.close, color: colors.onSurface),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'分享笔记',
|
'分享笔记',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
@@ -53,12 +54,12 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
|||||||
height: 20,
|
height: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Text(
|
: Text(
|
||||||
'分享',
|
'分享',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -78,6 +79,7 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPosterWidget() {
|
Widget _buildPosterWidget() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final note = widget.note;
|
final note = widget.note;
|
||||||
final hasImages = note.images.isNotEmpty;
|
final hasImages = note.images.isNotEmpty;
|
||||||
final dateStr =
|
final dateStr =
|
||||||
@@ -88,7 +90,7 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
|||||||
return Container(
|
return Container(
|
||||||
width: 320,
|
width: 320,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -124,10 +126,10 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
|||||||
if (note.title.isNotEmpty) ...[
|
if (note.title.isNotEmpty) ...[
|
||||||
Text(
|
Text(
|
||||||
note.title,
|
note.title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
@@ -136,9 +138,9 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
|||||||
// 日期
|
// 日期
|
||||||
Text(
|
Text(
|
||||||
dateStr,
|
dateStr,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -152,12 +154,12 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF0F0F0),
|
color: colors.outlineVariant,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
tag,
|
tag,
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF888888)),
|
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.5)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@@ -165,7 +167,7 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
|||||||
],
|
],
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Container(height: 0.5, color: const Color(0xFFE8E8E8)),
|
Container(height: 0.5, color: colors.outline),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 正文内容
|
// 正文内容
|
||||||
@@ -173,9 +175,9 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
|||||||
note.content,
|
note.content,
|
||||||
maxLines: 12,
|
maxLines: 12,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF333333),
|
color: colors.onSurface.withValues(alpha: 0.75),
|
||||||
height: 1.8,
|
height: 1.8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -191,11 +193,11 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
|||||||
],
|
],
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
// Mooknote 品牌
|
// Mooknote 品牌
|
||||||
const Text(
|
Text(
|
||||||
'Mooknote',
|
'Mooknote',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -210,14 +212,15 @@ class _NoteSharePageState extends State<NoteSharePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMetaChip(IconData icon, String text) {
|
Widget _buildMetaChip(IconData icon, String text) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 13, color: const Color(0xFFAAAAAA)),
|
Icon(icon, size: 13, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)),
|
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
bool _hasMore = true;
|
bool _hasMore = true;
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
int _layoutStyle = 0; // 0: 列表, 1: 瀑布流, 2: 时间线
|
int _layoutStyle = 0;
|
||||||
bool _firstLoad = true;
|
bool _firstLoad = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -110,7 +110,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// 笔记内容
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Consumer<AppProvider>(
|
child: Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
@@ -122,10 +121,11 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (allNotes.isEmpty && _displayedNotes.isEmpty) {
|
if (allNotes.isEmpty && _displayedNotes.isEmpty) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: _refresh,
|
onRefresh: _refresh,
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
children: [_buildEmptyState(context)],
|
children: [_buildEmptyState(context)],
|
||||||
@@ -159,6 +159,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildWaterfallSkeleton() {
|
Widget _buildWaterfallSkeleton() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -168,7 +169,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
children: List.generate(4, (_) => Container(
|
children: List.generate(4, (_) => Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 6, offset: const Offset(0, 2)),
|
BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 6, offset: const Offset(0, 2)),
|
||||||
@@ -200,25 +201,24 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 列表视图 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
Widget _buildListView() {
|
Widget _buildListView() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: _refresh,
|
onRefresh: _refresh,
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 100),
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 100),
|
||||||
itemCount: _displayedNotes.length + (_hasMore ? 1 : 0),
|
itemCount: _displayedNotes.length + (_hasMore ? 1 : 0),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index >= _displayedNotes.length) {
|
if (index >= _displayedNotes.length) {
|
||||||
return const Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 20, height: 20,
|
width: 20, height: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A)),
|
child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -229,24 +229,23 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 时间线视图 ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
Widget _buildTimelineView() {
|
Widget _buildTimelineView() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: _refresh,
|
onRefresh: _refresh,
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
||||||
itemCount: _displayedNotes.length + (_hasMore ? 1 : 0),
|
itemCount: _displayedNotes.length + (_hasMore ? 1 : 0),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index >= _displayedNotes.length) {
|
if (index >= _displayedNotes.length) {
|
||||||
return const Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SizedBox(width: 20, height: 20,
|
child: SizedBox(width: 20, height: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A))),
|
child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -257,6 +256,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTimelineItem(Note note) {
|
Widget _buildTimelineItem(Note note) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) async {
|
Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) async {
|
||||||
@@ -268,72 +268,60 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// 左侧时间线
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 40,
|
width: 40,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// 圆点
|
|
||||||
Container(
|
Container(
|
||||||
width: 10,
|
width: 10,
|
||||||
height: 10,
|
height: 10,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
border: Border.all(color: colors.surface, width: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 连线
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 1,
|
width: 1,
|
||||||
color: const Color(0xFFE5E5E5),
|
color: colors.outline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 右侧内容
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// 时间
|
|
||||||
Text(
|
Text(
|
||||||
_formatFullDate(note.updatedAt),
|
_formatFullDate(note.updatedAt),
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
|
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 标题
|
|
||||||
if (note.title.isNotEmpty) ...[
|
if (note.title.isNotEmpty) ...[
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
note.title,
|
note.title,
|
||||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
// 内容预览
|
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
_getPreviewText(note),
|
_getPreviewText(note),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 13, color: Color(0xFF888888), height: 1.5),
|
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5), height: 1.5),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 标签
|
|
||||||
if (note.tags.isNotEmpty) ...[
|
if (note.tags.isNotEmpty) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Wrap(
|
Wrap(
|
||||||
@@ -342,10 +330,10 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
children: note.tags.map((tag) => Container(
|
children: note.tags.map((tag) => Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(tag, style: const TextStyle(fontSize: 10, color: Color(0xFF999999))),
|
child: Text(tag, style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
)).toList(),
|
)).toList(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -364,10 +352,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
'${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
'${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 瀑布流视图 ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
Widget _buildWaterfallView() {
|
Widget _buildWaterfallView() {
|
||||||
// 分为左右两列
|
final colors = Theme.of(context).colorScheme;
|
||||||
final leftItems = <Note>[];
|
final leftItems = <Note>[];
|
||||||
final rightItems = <Note>[];
|
final rightItems = <Note>[];
|
||||||
for (int i = 0; i < _displayedNotes.length; i++) {
|
for (int i = 0; i < _displayedNotes.length; i++) {
|
||||||
@@ -380,8 +366,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: _refresh,
|
onRefresh: _refresh,
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
||||||
@@ -398,6 +384,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildWaterfallCard(Note note) {
|
Widget _buildWaterfallCard(Note note) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final contentText = _getPreviewText(note);
|
final contentText = _getPreviewText(note);
|
||||||
final images = note.images;
|
final images = note.images;
|
||||||
final hasImage = images.isNotEmpty;
|
final hasImage = images.isNotEmpty;
|
||||||
@@ -413,7 +400,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -428,7 +415,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// 顶部图片
|
|
||||||
if (hasImage)
|
if (hasImage)
|
||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -458,8 +444,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// 底部文字区域
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(10, 8, 10, 10),
|
padding: const EdgeInsets.fromLTRB(10, 8, 10, 10),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -471,47 +455,45 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
note.title,
|
note.title,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.3,
|
height: 1.3,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
if (contentText.isNotEmpty && contentText != '(无内容)') ...[
|
if (contentText.isNotEmpty && contentText != '(无内容)') ...[
|
||||||
if (note.title.isNotEmpty) const SizedBox(height: 4),
|
if (note.title.isNotEmpty) const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
contentText,
|
contentText,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_formatTime(note.updatedAt),
|
_formatTime(note.updatedAt),
|
||||||
style: const TextStyle(fontSize: 10, color: Color(0xFFCCCCCC)),
|
style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (note.tags.isNotEmpty)
|
if (note.tags.isNotEmpty)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(3),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
note.tags.first,
|
note.tags.first,
|
||||||
style: const TextStyle(fontSize: 10, color: Color(0xFF999999)),
|
style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -546,20 +528,21 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showDeleteDialog(BuildContext context, Note note) {
|
void _showDeleteDialog(BuildContext context, Note note) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: const Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
content: const Text('确定要删除这条笔记吗?删除后可在回收站恢复。',
|
content: Text('确定要删除这条笔记吗?删除后可在回收站恢复。',
|
||||||
style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)),
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFF666666),
|
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
child: const Text('取消'),
|
child: const Text('取消'),
|
||||||
@@ -570,8 +553,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: colors.error,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colors.onError,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
@@ -584,8 +567,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 数据同步 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
void _syncDisplayedNotes(List<Note> allNotes) {
|
void _syncDisplayedNotes(List<Note> allNotes) {
|
||||||
final validNoteIds = allNotes.map((n) => n.id).toSet();
|
final validNoteIds = allNotes.map((n) => n.id).toSet();
|
||||||
final initialLength = _displayedNotes.length;
|
final initialLength = _displayedNotes.length;
|
||||||
@@ -611,9 +592,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 空状态 ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
Widget _buildEmptyState(BuildContext context) {
|
Widget _buildEmptyState(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -621,24 +601,24 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
Container(
|
Container(
|
||||||
width: 80, height: 80,
|
width: 80, height: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.note_outlined, size: 40, color: Color(0xFFCCCCCC)),
|
child: Icon(Icons.note_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Text('暂无笔记', style: TextStyle(fontSize: 16, color: Color(0xFF999999))),
|
Text('暂无笔记', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => Navigator.pushNamed(context, '/note-form'),
|
onTap: () => Navigator.pushNamed(context, '/note-form'),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Text('添加记录',
|
child: Text('添加记录',
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -51,12 +51,13 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
AppBar(
|
AppBar(
|
||||||
leading: Builder(
|
leading: Builder(
|
||||||
builder: (context) => IconButton(
|
builder: (context) => IconButton(
|
||||||
icon: const Icon(Icons.menu, color: Color(0xFF1A1A1A)),
|
icon: Icon(Icons.menu, color: colors.onSurface),
|
||||||
onPressed: () => Scaffold.of(context).openDrawer(),
|
onPressed: () => Scaffold.of(context).openDrawer(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -64,7 +65,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHigh,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -98,6 +99,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
Widget _buildUserCard() {
|
Widget _buildUserCard() {
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final movieCount = provider.movies.where((m) => !m.isDeleted).length;
|
final movieCount = provider.movies.where((m) => !m.isDeleted).length;
|
||||||
final bookCount = provider.books.length;
|
final bookCount = provider.books.length;
|
||||||
final noteCount = provider.notes.length;
|
final noteCount = provider.notes.length;
|
||||||
@@ -106,7 +108,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
margin: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
margin: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)),
|
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)),
|
||||||
@@ -122,14 +124,14 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
width: 64, height: 64,
|
width: 64, height: 64,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: _avatarPath != null && _avatarPath!.isNotEmpty
|
child: _avatarPath != null && _avatarPath!.isNotEmpty
|
||||||
? Image.file(File(_avatarPath!), fit: BoxFit.cover,
|
? Image.file(File(_avatarPath!), fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) => const Icon(Icons.person_outline, size: 32, color: Color(0xFFCCCCCC)))
|
errorBuilder: (_, __, ___) => Icon(Icons.person_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.25)))
|
||||||
: const Icon(Icons.person_outline, size: 32, color: Color(0xFFCCCCCC)),
|
: Icon(Icons.person_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
@@ -139,25 +141,25 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => _editNickname(context),
|
onTap: () => _editNickname(context),
|
||||||
child: Text(_nickname, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
child: Text(_nickname, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => _editMotto(context),
|
onTap: () => _editMotto(context),
|
||||||
child: Text(_motto, maxLines: 1, overflow: TextOverflow.ellipsis,
|
child: Text(_motto, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 13, color: Color(0xFFAAAAAA))),
|
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.settings_outlined, color: Color(0xFF999999)),
|
icon: Icon(Icons.settings_outlined, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
onPressed: () => _showSettings(context),
|
onPressed: () => _showSettings(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Divider(height: 1, color: Color(0xFFF0F0F0)),
|
Divider(height: 1, color: colors.outlineVariant),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildStatRow(Icons.movie_outlined, _formatCount(movieCount), '观影'),
|
_buildStatRow(Icons.movie_outlined, _formatCount(movieCount), '观影'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -172,13 +174,14 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatRow(IconData icon, String count, String label) {
|
Widget _buildStatRow(IconData icon, String count, String label) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 14, color: const Color(0xFFBBBBBB)),
|
Icon(icon, size: 14, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
Text(label, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(count, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
Text(count, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -192,11 +195,12 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
// ─── 探索行 ──────────────────────────────────────────────────────────
|
// ─── 探索行 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildExploreRow() {
|
Widget _buildExploreRow() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)),
|
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)),
|
||||||
@@ -204,9 +208,9 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.auto_awesome, size: 16, color: Color(0xFFBBBBBB)),
|
Icon(Icons.auto_awesome, size: 16, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
const Text('快捷入口', style: TextStyle(fontSize: 13, color: Color(0xFF888888))),
|
Text('快捷入口', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
_buildQuickAction(Icons.explore_outlined, '漫步', () => Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage()))),
|
_buildQuickAction(Icons.explore_outlined, '漫步', () => Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage()))),
|
||||||
const SizedBox(width: 24),
|
const SizedBox(width: 24),
|
||||||
@@ -217,14 +221,15 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildQuickAction(IconData icon, String label, VoidCallback onTap) {
|
Widget _buildQuickAction(IconData icon, String label, VoidCallback onTap) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 20, color: const Color(0xFF555555)),
|
Icon(icon, size: 20, color: colors.onSurface.withValues(alpha: 0.7)),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(label, style: const TextStyle(fontSize: 10, color: Color(0xFF999999))),
|
Text(label, style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -233,17 +238,19 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
// ─── 菜单 ────────────────────────────────────────────────────────────
|
// ─── 菜单 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildSectionHeader(String title) {
|
Widget _buildSectionHeader(String title) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 10),
|
padding: const EdgeInsets.fromLTRB(24, 0, 24, 10),
|
||||||
child: Text(title, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFFBBBBBB))),
|
child: Text(title, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMenuGroup(List<_MenuEntry> entries) {
|
Widget _buildMenuGroup(List<_MenuEntry> entries) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)),
|
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)),
|
||||||
@@ -269,19 +276,19 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
Container(
|
Container(
|
||||||
width: 36, height: 36,
|
width: 36, height: 36,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Icon(e.value.icon, size: 18, color: const Color(0xFF666666)),
|
child: Icon(e.value.icon, size: 18, color: colors.onSurface.withValues(alpha: 0.6)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(child: Text(e.value.title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))),
|
Expanded(child: Text(e.value.title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface))),
|
||||||
const Icon(Icons.chevron_right, size: 16, color: Color(0xFFD0D0D0)),
|
Icon(Icons.chevron_right, size: 16, color: colors.onSurface.withValues(alpha: 0.2)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!isLast) const Divider(height: 1, indent: 68, endIndent: 20, color: Color(0xFFF0F0F0)),
|
if (!isLast) Divider(height: 1, indent: 68, endIndent: 20, color: colors.outlineVariant),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@@ -310,16 +317,17 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _editNickname(BuildContext context) {
|
void _editNickname(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final controller = TextEditingController(text: _nickname);
|
final controller = TextEditingController(text: _nickname);
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
backgroundColor: Colors.white, elevation: 0,
|
backgroundColor: colors.surface, elevation: 0,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
title: const Text('修改昵称', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
title: Text('修改昵称', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
content: TextField(controller: controller, decoration: const InputDecoration(hintText: '输入昵称', border: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xFFE5E5E5))))),
|
content: TextField(controller: controller, decoration: InputDecoration(hintText: '输入昵称', border: UnderlineInputBorder(borderSide: BorderSide(color: colors.outline)))),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消', style: TextStyle(color: Color(0xFF666666)))),
|
TextButton(onPressed: () => Navigator.pop(context), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
|
||||||
TextButton(onPressed: () async {
|
TextButton(onPressed: () async {
|
||||||
final newNickname = controller.text.trim();
|
final newNickname = controller.text.trim();
|
||||||
if (newNickname.isNotEmpty) {
|
if (newNickname.isNotEmpty) {
|
||||||
@@ -334,16 +342,17 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _editMotto(BuildContext context) {
|
void _editMotto(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final controller = TextEditingController(text: _motto);
|
final controller = TextEditingController(text: _motto);
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
backgroundColor: Colors.white, elevation: 0,
|
backgroundColor: colors.surface, elevation: 0,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
title: const Text('修改座右铭', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
title: Text('修改座右铭', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
content: TextField(controller: controller, maxLines: 2, decoration: const InputDecoration(hintText: '输入座右铭', border: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xFFE5E5E5))))),
|
content: TextField(controller: controller, maxLines: 2, decoration: InputDecoration(hintText: '输入座右铭', border: UnderlineInputBorder(borderSide: BorderSide(color: colors.outline)))),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消', style: TextStyle(color: Color(0xFF666666)))),
|
TextButton(onPressed: () => Navigator.pop(context), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
|
||||||
TextButton(onPressed: () async {
|
TextButton(onPressed: () async {
|
||||||
final newMotto = controller.text.trim();
|
final newMotto = controller.text.trim();
|
||||||
await _userPrefs.setMotto(newMotto);
|
await _userPrefs.setMotto(newMotto);
|
||||||
@@ -358,34 +367,35 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
// ─── 备份弹窗 ────────────────────────────────────────────────────────
|
// ─── 备份弹窗 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
void _showBackupOptions(BuildContext context) {
|
void _showBackupOptions(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||||
builder: (ctx) => Padding(
|
builder: (ctx) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Container(width: 36, height: 4, decoration: BoxDecoration(color: const Color(0xFFDDDDDD), borderRadius: BorderRadius.circular(2))),
|
Container(width: 36, height: 4, decoration: BoxDecoration(color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Align(alignment: Alignment.centerLeft, child: Text('选择备份方式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)))),
|
Align(alignment: Alignment.centerLeft, child: Text('选择备份方式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface))),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.folder_outlined, color: Color(0xFF666666))),
|
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.folder_outlined, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
title: const Text('本地备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
title: Text('本地备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
subtitle: const Text('备份到本地文件夹,支持恢复', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
subtitle: Text('备份到本地文件夹,支持恢复', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
trailing: const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC)),
|
trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
onTap: () { Navigator.pop(ctx); _push(context, const BackupPage()); },
|
onTap: () { Navigator.pop(ctx); _push(context, const BackupPage()); },
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5, color: Color(0xFFF0F0F0)),
|
Divider(height: 0.5, color: colors.outlineVariant),
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.cloud_outlined, color: Color(0xFF666666))),
|
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.cloud_outlined, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
title: const Text('云备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
title: Text('云备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
subtitle: const Text('通过 WebDAV 同步到云端', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
subtitle: Text('通过 WebDAV 同步到云端', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
trailing: const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC)),
|
trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
onTap: () { Navigator.pop(ctx); _push(context, const CloudSyncPage()); },
|
onTap: () { Navigator.pop(ctx); _push(context, const CloudSyncPage()); },
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
@@ -421,17 +431,20 @@ class SettingsPage extends StatefulWidget {
|
|||||||
class _SettingsPageState extends State<SettingsPage> {
|
class _SettingsPageState extends State<SettingsPage> {
|
||||||
final UserPrefs _userPrefs = UserPrefs();
|
final UserPrefs _userPrefs = UserPrefs();
|
||||||
bool _hideBottomNavOnScroll = true;
|
bool _hideBottomNavOnScroll = true;
|
||||||
|
int _themeMode = 0; // 0=系统, 1=浅色, 2=深色
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_hideBottomNavOnScroll = _userPrefs.hideBottomNavOnScroll;
|
_hideBottomNavOnScroll = _userPrefs.hideBottomNavOnScroll;
|
||||||
|
_themeMode = _userPrefs.themeMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(title: const Text('设置')),
|
appBar: AppBar(title: const Text('设置')),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
@@ -442,7 +455,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
subtitle: '清理未在数据库中引用的图片文件',
|
subtitle: '清理未在数据库中引用的图片文件',
|
||||||
onTap: () => _showClearCacheDialog(context),
|
onTap: () => _showClearCacheDialog(context),
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildSwitchItem(
|
_buildSwitchItem(
|
||||||
icon: Icons.swipe_vertical_outlined,
|
icon: Icons.swipe_vertical_outlined,
|
||||||
title: '底部导航栏滚动隐藏',
|
title: '底部导航栏滚动隐藏',
|
||||||
@@ -450,28 +463,31 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
value: _hideBottomNavOnScroll,
|
value: _hideBottomNavOnScroll,
|
||||||
onChanged: _toggleHideBottomNavOnScroll,
|
onChanged: _toggleHideBottomNavOnScroll,
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildNavigationItem(
|
_buildNavigationItem(
|
||||||
icon: Icons.apps_outlined,
|
icon: Icons.apps_outlined,
|
||||||
title: '应用图标',
|
title: '应用图标',
|
||||||
subtitle: '更换桌面应用图标',
|
subtitle: '更换桌面应用图标',
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AppIconPickerPage())),
|
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AppIconPickerPage())),
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildNavigationItem(
|
_buildNavigationItem(
|
||||||
icon: Icons.view_list_outlined,
|
icon: Icons.view_list_outlined,
|
||||||
title: '主界面设置',
|
title: '主界面设置',
|
||||||
subtitle: '启动标签、模块显示开关',
|
subtitle: '启动标签、模块显示开关',
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MainContentSettingsPage())),
|
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MainContentSettingsPage())),
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildNavigationItem(
|
_buildNavigationItem(
|
||||||
icon: Icons.dashboard_outlined,
|
icon: Icons.dashboard_outlined,
|
||||||
title: '布局设置',
|
title: '布局设置',
|
||||||
subtitle: '笔记、影视、阅读的展示样式',
|
subtitle: '笔记、影视、阅读的展示样式',
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const LayoutSettingsPage())),
|
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const LayoutSettingsPage())),
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
|
_buildSectionHeader('外观'),
|
||||||
|
_buildThemeModeSelector(),
|
||||||
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildSectionHeader('帮助'),
|
_buildSectionHeader('帮助'),
|
||||||
_buildLinkItem(
|
_buildLinkItem(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -480,7 +496,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
subtitle: '查看应用使用指南',
|
subtitle: '查看应用使用指南',
|
||||||
url: 'https://mooknote.iletter.top/#/guide',
|
url: 'https://mooknote.iletter.top/#/guide',
|
||||||
),
|
),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -491,23 +507,93 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
setState(() => _hideBottomNavOnScroll = value);
|
setState(() => _hideBottomNavOnScroll = value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const _themeModeLabels = ['跟随系统', '浅色模式', '深色模式'];
|
||||||
|
static const _themeModeIcons = [Icons.brightness_auto, Icons.light_mode, Icons.dark_mode];
|
||||||
|
|
||||||
|
Widget _buildThemeModeSelector() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => _showThemeModePicker(),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(_themeModeIcons[_themeMode], color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text('主题模式', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(_themeModeLabels[_themeMode], style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showThemeModePicker() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (ctx) => Container(
|
||||||
|
decoration: BoxDecoration(color: colors.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(16))),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(width: 36, height: 4, decoration: BoxDecoration(color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Align(alignment: Alignment.centerLeft, child: Padding(padding: const EdgeInsets.symmetric(horizontal: 24), child: Text('主题模式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)))),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
for (int i = 0; i < _themeModeLabels.length; i++)
|
||||||
|
ListTile(
|
||||||
|
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(_themeModeIcons[i], color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
|
title: Text(_themeModeLabels[i], style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
|
trailing: _themeMode == i ? Icon(Icons.check, color: colors.onSurface, size: 20) : null,
|
||||||
|
onTap: () async { await _setThemeMode(i); Navigator.pop(ctx); },
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setThemeMode(int mode) async {
|
||||||
|
await _userPrefs.setThemeMode(mode);
|
||||||
|
final themeMode = switch (mode) {
|
||||||
|
1 => ThemeMode.light,
|
||||||
|
2 => ThemeMode.dark,
|
||||||
|
_ => ThemeMode.system,
|
||||||
|
};
|
||||||
|
if (mounted) {
|
||||||
|
context.read<AppProvider>().setThemeMode(themeMode);
|
||||||
|
setState(() => _themeMode = mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildSwitchItem({required IconData icon, required String title, required String subtitle, required bool value, required ValueChanged<bool> onChanged}) {
|
Widget _buildSwitchItem({required IconData icon, required String title, required String subtitle, required bool value, required ValueChanged<bool> onChanged}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => onChanged(!value),
|
onTap: () => onChanged(!value),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)),
|
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
Switch(value: value, onChanged: onChanged, activeColor: const Color(0xFF1A1A1A), activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), inactiveThumbColor: Colors.white, inactiveTrackColor: const Color(0xFFE5E5E5)),
|
Switch(value: value, onChanged: onChanged, activeColor: colors.primary, activeTrackColor: colors.primary.withOpacity(0.3), inactiveThumbColor: colors.surface, inactiveTrackColor: colors.outline),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -515,29 +601,31 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSectionHeader(String title) {
|
Widget _buildSectionHeader(String title) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(24, 32, 24, 12),
|
padding: const EdgeInsets.fromLTRB(24, 32, 24, 12),
|
||||||
child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNavigationItem({required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
|
Widget _buildNavigationItem({required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)),
|
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC), size: 20),
|
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -545,22 +633,23 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLinkItem({required BuildContext context, required IconData icon, required String title, required String subtitle, required String url}) {
|
Widget _buildLinkItem({required BuildContext context, required IconData icon, required String title, required String subtitle, required String url}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => _launchUrl(context, url),
|
onTap: () => _launchUrl(context, url),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)),
|
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
const Icon(Icons.open_in_new, color: Color(0xFFCCCCCC), size: 18),
|
Icon(Icons.open_in_new, color: colors.onSurface.withValues(alpha: 0.25), size: 18),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -568,19 +657,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActionItem({required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
|
Widget _buildActionItem({required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)),
|
Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -590,19 +680,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showClearCacheDialog(BuildContext pageContext) {
|
void _showClearCacheDialog(BuildContext pageContext) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: pageContext,
|
context: pageContext,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
backgroundColor: Colors.white, elevation: 0,
|
backgroundColor: colors.surface, elevation: 0,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
title: const Text('清除缓存数据'),
|
title: const Text('清除缓存数据'),
|
||||||
content: const Text('这将删除所有未在数据库中引用的图片文件。确定要继续吗?'),
|
content: const Text('这将删除所有未在数据库中引用的图片文件。确定要继续吗?'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('取消', style: TextStyle(color: Color(0xFF666666)))),
|
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
|
||||||
TextButton(onPressed: () async {
|
TextButton(onPressed: () async {
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
await _clearCacheData(pageContext);
|
await _clearCacheData(pageContext);
|
||||||
}, child: const Text('确定', style: TextStyle(color: Colors.red))),
|
}, child: Text('确定', style: TextStyle(color: colors.error))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -712,24 +803,25 @@ class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(title: const Text('主界面设置')),
|
appBar: AppBar(title: const Text('主界面设置')),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
_buildSectionHeader('启动设置'),
|
_buildSectionHeader('启动设置'),
|
||||||
_buildDefaultTabSelector(),
|
_buildDefaultTabSelector(),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildSectionHeader('模块开关'),
|
_buildSectionHeader('模块开关'),
|
||||||
_buildSwitchItem(Icons.movie_outlined, '观影', '记录和管理观影记录', _showMovieTab, _toggleMovieTab),
|
_buildSwitchItem(Icons.movie_outlined, '观影', '记录和管理观影记录', _showMovieTab, _toggleMovieTab),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildSwitchItem(Icons.menu_book_outlined, '阅读', '记录和管理阅读记录', _showBookTab, _toggleBookTab),
|
_buildSwitchItem(Icons.menu_book_outlined, '阅读', '记录和管理阅读记录', _showBookTab, _toggleBookTab),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildSwitchItem(Icons.note_outlined, '笔记', '记录和管理笔记', _showNoteTab, _toggleNoteTab),
|
_buildSwitchItem(Icons.note_outlined, '笔记', '记录和管理笔记', _showNoteTab, _toggleNoteTab),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||||
child: const Text('至少保留一个模块,关闭后对应标签页将不再显示。', style: TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))),
|
child: Text('至少保留一个模块,关闭后对应标签页将不再显示。', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -737,37 +829,40 @@ class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSectionHeader(String title) {
|
Widget _buildSectionHeader(String title) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12),
|
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12),
|
||||||
child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSwitchItem(IconData icon, String title, String subtitle, bool value, ValueChanged<bool> onChanged) {
|
Widget _buildSwitchItem(IconData icon, String title, String subtitle, bool value, ValueChanged<bool> onChanged) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
||||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)),
|
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||||
title: Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
title: Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
subtitle: Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
trailing: Switch(value: value, onChanged: onChanged, activeColor: const Color(0xFF1A1A1A), activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), inactiveThumbColor: Colors.white, inactiveTrackColor: const Color(0xFFE5E5E5)),
|
trailing: Switch(value: value, onChanged: onChanged, activeColor: colors.primary, activeTrackColor: colors.primary.withOpacity(0.3), inactiveThumbColor: colors.surface, inactiveTrackColor: colors.outline),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDefaultTabSelector() {
|
Widget _buildDefaultTabSelector() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final labels = ['影视', '阅读', '笔记'];
|
final labels = ['影视', '阅读', '笔记'];
|
||||||
final icons = [Icons.movie_outlined, Icons.menu_book_outlined, Icons.note_outlined];
|
final icons = [Icons.movie_outlined, Icons.menu_book_outlined, Icons.note_outlined];
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
||||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.home_outlined, color: Color(0xFF666666), size: 22)),
|
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.home_outlined, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||||
title: const Text('默认启动标签', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
title: Text('默认启动标签', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
subtitle: const Text('打开应用时默认显示的页面', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
subtitle: Text('打开应用时默认显示的页面', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(labels[_defaultTabIndex], style: const TextStyle(fontSize: 14, color: Color(0xFF999999))),
|
Text(labels[_defaultTabIndex], style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC), size: 20),
|
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () => _showDefaultTabPicker(labels, icons),
|
onTap: () => _showDefaultTabPicker(labels, icons),
|
||||||
@@ -775,24 +870,25 @@ class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showDefaultTabPicker(List<String> labels, List<IconData> icons) {
|
void _showDefaultTabPicker(List<String> labels, List<IconData> icons) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
builder: (ctx) => Container(
|
builder: (ctx) => Container(
|
||||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
decoration: BoxDecoration(color: colors.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(16))),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Container(width: 36, height: 4, decoration: BoxDecoration(color: const Color(0xFFDDDDDD), borderRadius: BorderRadius.circular(2))),
|
Container(width: 36, height: 4, decoration: BoxDecoration(color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Align(alignment: Alignment.centerLeft, child: Padding(padding: EdgeInsets.symmetric(horizontal: 24), child: Text('默认启动标签', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))))),
|
Align(alignment: Alignment.centerLeft, child: Padding(padding: const EdgeInsets.symmetric(horizontal: 24), child: Text('默认启动标签', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)))),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
for (int i = 0; i < labels.length; i++)
|
for (int i = 0; i < labels.length; i++)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icons[i], color: const Color(0xFF666666))),
|
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icons[i], color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
title: Text(labels[i], style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
title: Text(labels[i], style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
trailing: _defaultTabIndex == i ? const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 20) : null,
|
trailing: _defaultTabIndex == i ? Icon(Icons.check, color: colors.onSurface, size: 20) : null,
|
||||||
onTap: () async { await _userPrefs.setDefaultMainTabIndex(i); setState(() => _defaultTabIndex = i); Navigator.pop(ctx); },
|
onTap: () async { await _userPrefs.setDefaultMainTabIndex(i); setState(() => _defaultTabIndex = i); Navigator.pop(ctx); },
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -828,28 +924,29 @@ class _LayoutSettingsPageState extends State<LayoutSettingsPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(title: const Text('布局设置')),
|
appBar: AppBar(title: const Text('布局设置')),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
_buildSectionHeader('笔记布局'),
|
_buildSectionHeader('笔记布局'),
|
||||||
_buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列列表,简洁清晰', value: 0, groupValue: _noteLayout, onTap: () => _setLayout('note', 0)),
|
_buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列列表,简洁清晰', value: 0, groupValue: _noteLayout, onTap: () => _setLayout('note', 0)),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildLayoutOption(icon: Icons.grid_view_outlined, title: '瀑布流布局', subtitle: '双列卡片,图文并茂', value: 1, groupValue: _noteLayout, onTap: () => _setLayout('note', 1)),
|
_buildLayoutOption(icon: Icons.grid_view_outlined, title: '瀑布流布局', subtitle: '双列卡片,图文并茂', value: 1, groupValue: _noteLayout, onTap: () => _setLayout('note', 1)),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildLayoutOption(icon: Icons.timeline_outlined, title: '时间线布局', subtitle: '按时间排列,纵览全局', value: 2, groupValue: _noteLayout, onTap: () => _setLayout('note', 2)),
|
_buildLayoutOption(icon: Icons.timeline_outlined, title: '时间线布局', subtitle: '按时间排列,纵览全局', value: 2, groupValue: _noteLayout, onTap: () => _setLayout('note', 2)),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildSectionHeader('影视布局'),
|
_buildSectionHeader('影视布局'),
|
||||||
_buildLayoutOption(icon: Icons.grid_view_outlined, title: '海报网格', subtitle: '三列海报,赏心悦目', value: 0, groupValue: _movieLayout, onTap: () => _setLayout('movie', 0)),
|
_buildLayoutOption(icon: Icons.grid_view_outlined, title: '海报网格', subtitle: '三列海报,赏心悦目', value: 0, groupValue: _movieLayout, onTap: () => _setLayout('movie', 0)),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列卡片,信息一览', value: 1, groupValue: _movieLayout, onTap: () => _setLayout('movie', 1)),
|
_buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列卡片,信息一览', value: 1, groupValue: _movieLayout, onTap: () => _setLayout('movie', 1)),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildSectionHeader('阅读布局'),
|
_buildSectionHeader('阅读布局'),
|
||||||
_buildLayoutOption(icon: Icons.grid_view_outlined, title: '封面网格', subtitle: '三列封面,清新雅致', value: 0, groupValue: _bookLayout, onTap: () => _setLayout('book', 0)),
|
_buildLayoutOption(icon: Icons.grid_view_outlined, title: '封面网格', subtitle: '三列封面,清新雅致', value: 0, groupValue: _bookLayout, onTap: () => _setLayout('book', 0)),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
_buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列卡片,信息一览', value: 1, groupValue: _bookLayout, onTap: () => _setLayout('book', 1)),
|
_buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列卡片,信息一览', value: 1, groupValue: _bookLayout, onTap: () => _setLayout('book', 1)),
|
||||||
const Divider(height: 0.5, indent: 24, endIndent: 24),
|
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -864,20 +961,22 @@ class _LayoutSettingsPageState extends State<LayoutSettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSectionHeader(String title) {
|
Widget _buildSectionHeader(String title) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12),
|
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12),
|
||||||
child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLayoutOption({required IconData icon, required String title, required String subtitle, required int value, required int groupValue, required VoidCallback onTap}) {
|
Widget _buildLayoutOption({required IconData icon, required String title, required String subtitle, required int value, required int groupValue, required VoidCallback onTap}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final selected = value == groupValue;
|
final selected = value == groupValue;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
||||||
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)),
|
leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)),
|
||||||
title: Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
title: Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
subtitle: Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
trailing: selected ? const Icon(Icons.check_circle, color: Color(0xFF1A1A1A), size: 20) : const Icon(Icons.circle_outlined, color: Color(0xFFDDDDDD), size: 20),
|
trailing: selected ? Icon(Icons.check_circle, color: colors.onSurface, size: 20) : Icon(Icons.circle_outlined, color: colors.onSurface.withValues(alpha: 0.15), size: 20),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -911,10 +1010,11 @@ class _WebViewPageState extends State<WebViewPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(title: const Text(''), actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: () => _controller.reload())]),
|
appBar: AppBar(title: const Text(''), actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: () => _controller.reload())]),
|
||||||
body: Stack(children: [WebViewWidget(controller: _controller), if (_isLoading) const Center(child: CircularProgressIndicator(color: Color(0xFF999999)))]),
|
body: Stack(children: [WebViewWidget(controller: _controller), if (_isLoading) Center(child: CircularProgressIndicator(color: colors.onSurface.withValues(alpha: 0.4)))]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class _DeletedItem {
|
|||||||
|
|
||||||
class _RecycleBinPageState extends State<RecycleBinPage> {
|
class _RecycleBinPageState extends State<RecycleBinPage> {
|
||||||
List<_DeletedItem> _allItems = [];
|
List<_DeletedItem> _allItems = [];
|
||||||
_ItemType? _filterType; // null = 全部
|
_ItemType? _filterType;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
|
|
||||||
List<_DeletedItem> get _filteredItems =>
|
List<_DeletedItem> get _filteredItems =>
|
||||||
@@ -80,8 +80,9 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('回收站'),
|
title: const Text('回收站'),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -91,12 +92,12 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: _showClearAllDialog,
|
onPressed: _showClearAllDialog,
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
foregroundColor: Colors.red,
|
foregroundColor: Colors.red,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
side: const BorderSide(color: Color(0xFFFFDDDD), width: 0.5),
|
side: BorderSide(color: colors.error.withValues(alpha: 0.15), width: 0.5),
|
||||||
),
|
),
|
||||||
minimumSize: Size.zero,
|
minimumSize: Size.zero,
|
||||||
),
|
),
|
||||||
@@ -106,12 +107,10 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: _isLoading
|
body: _isLoading
|
||||||
? const Center(child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A)))
|
? Center(child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary))
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
// 筛选标签行
|
|
||||||
if (_allItems.isNotEmpty) _buildFilterRow(),
|
if (_allItems.isNotEmpty) _buildFilterRow(),
|
||||||
// 列表
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _filteredItems.isEmpty
|
child: _filteredItems.isEmpty
|
||||||
? _buildEmptyState()
|
? _buildEmptyState()
|
||||||
@@ -130,11 +129,12 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFilterRow() {
|
Widget _buildFilterRow() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(bottom: BorderSide(color: Color(0xFFEEEEEE), width: 0.5)),
|
border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5)),
|
||||||
),
|
),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
@@ -149,6 +149,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _filterChip(String label, _ItemType? type) {
|
Widget _filterChip(String label, _ItemType? type) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final active = _filterType == type;
|
final active = _filterType == type;
|
||||||
final count = type == null ? _allItems.length : _allItems.where((i) => i.type == type).length;
|
final count = type == null ? _allItems.length : _allItems.where((i) => i.type == type).length;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@@ -156,7 +157,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: active ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5),
|
color: active ? colors.primary : colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -164,7 +165,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: active ? Colors.white : const Color(0xFF888888),
|
color: active ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -172,6 +173,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCard(_DeletedItem item) {
|
Widget _buildCard(_DeletedItem item) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Dismissible(
|
return Dismissible(
|
||||||
key: Key('${item.type.name}_${item.id}'),
|
key: Key('${item.type.name}_${item.id}'),
|
||||||
direction: DismissDirection.endToStart,
|
direction: DismissDirection.endToStart,
|
||||||
@@ -181,9 +183,9 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||||
@@ -193,11 +195,11 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Icon(item.icon, size: 20, color: const Color(0xFF888888)),
|
child: Icon(item.icon, size: 20, color: colors.onSurface.withValues(alpha: 0.5)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -210,7 +212,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
item.title,
|
item.title,
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A), height: 1.3),
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface, height: 1.3),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -219,13 +221,13 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(3),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
item.typeLabel,
|
item.typeLabel,
|
||||||
style: const TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: Color(0xFF999999)),
|
style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -233,15 +235,15 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text(
|
Text(
|
||||||
item.subtitle,
|
item.subtitle,
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)),
|
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_actionBtn(Icons.restore, '恢复', const Color(0xFF1A1A1A), () => _restore(item)),
|
_actionBtn(Icons.restore, '恢复', colors.primary, () => _restore(item)),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
_actionBtn(Icons.delete_outline, '删除', Colors.red, () => _permanentDelete(item)),
|
_actionBtn(Icons.delete_outline, '删除', colors.error, () => _permanentDelete(item)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -263,6 +265,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _actionBtn(IconData icon, String tooltip, Color color, VoidCallback onTap) {
|
Widget _actionBtn(IconData icon, String tooltip, Color color, VoidCallback onTap) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@@ -274,9 +277,9 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Icon(icon, size: 16, color: color),
|
child: Icon(icon, size: 16, color: color),
|
||||||
),
|
),
|
||||||
@@ -286,6 +289,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState() {
|
Widget _buildEmptyState() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -294,18 +298,18 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
width: 72,
|
width: 72,
|
||||||
height: 72,
|
height: 72,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(18),
|
borderRadius: BorderRadius.circular(18),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.delete_outline, size: 32, color: Color(0xFFD5D5D5)),
|
child: Icon(Icons.delete_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.15)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
_filterType == null ? '回收站是空的' : '没有删除的项目',
|
_filterType == null ? '回收站是空的' : '没有删除的项目',
|
||||||
style: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)),
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
const Text('删除的项目会显示在这里', style: TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))),
|
Text('删除的项目会显示在这里', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -344,25 +348,26 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _showConfirmDialog(String message) async {
|
Future<bool> _showConfirmDialog(String message) async {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final result = await showDialog<bool>(
|
final result = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: const Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
content: Text(message, style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)),
|
content: Text(message, style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx, false),
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
style: TextButton.styleFrom(foregroundColor: const Color(0xFF666666)),
|
style: TextButton.styleFrom(foregroundColor: colors.onSurface.withValues(alpha: 0.6)),
|
||||||
child: const Text('取消'),
|
child: const Text('取消'),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => Navigator.pop(ctx, true),
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: colors.error,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colors.onError,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
),
|
),
|
||||||
@@ -377,27 +382,28 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
|
|
||||||
void _showClearAllDialog() {
|
void _showClearAllDialog() {
|
||||||
final pageContext = context;
|
final pageContext = context;
|
||||||
|
final colors = Theme.of(pageContext).colorScheme;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: pageContext,
|
context: pageContext,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: const Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
|
const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('清空回收站', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
Text('清空回收站', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
content: const Text(
|
content: Text(
|
||||||
'确定要清空回收站吗?所有项目将被彻底删除,此操作不可恢复。',
|
'确定要清空回收站吗?所有项目将被彻底删除,此操作不可恢复。',
|
||||||
style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5),
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx),
|
onPressed: () => Navigator.pop(ctx),
|
||||||
style: TextButton.styleFrom(foregroundColor: const Color(0xFF666666)),
|
style: TextButton.styleFrom(foregroundColor: colors.onSurface.withValues(alpha: 0.6)),
|
||||||
child: const Text('取消'),
|
child: const Text('取消'),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
@@ -408,8 +414,8 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
|
|||||||
if (mounted) ToastUtil.show(pageContext, '回收站已清空');
|
if (mounted) ToastUtil.show(pageContext, '回收站已清空');
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: colors.error,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colors.onError,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -100,8 +100,9 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('搜索'),
|
title: const Text('搜索'),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@@ -121,18 +122,19 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSearchBar() {
|
Widget _buildSearchBar() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 4),
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 4),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
|
style: TextStyle(fontSize: 15, color: colors.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '搜索标题、别名、内容...',
|
hintText: '搜索标题、别名、内容...',
|
||||||
hintStyle: const TextStyle(color: Color(0xFFB0B0B0), fontSize: 15),
|
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.35), fontSize: 15),
|
||||||
prefixIcon: const Padding(
|
prefixIcon: Padding(
|
||||||
padding: EdgeInsets.only(left: 12, right: 8),
|
padding: const EdgeInsets.only(left: 12, right: 8),
|
||||||
child: Icon(Icons.search, color: Color(0xFF1A1A1A), size: 22),
|
child: Icon(Icons.search, color: colors.onSurface, size: 22),
|
||||||
),
|
),
|
||||||
prefixIconConstraints: const BoxConstraints(minWidth: 42, minHeight: 42),
|
prefixIconConstraints: const BoxConstraints(minWidth: 42, minHeight: 42),
|
||||||
suffixIcon: _searchController.text.isNotEmpty
|
suffixIcon: _searchController.text.isNotEmpty
|
||||||
@@ -147,15 +149,15 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
width: 28,
|
width: 28,
|
||||||
height: 28,
|
height: 28,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFE5E5E5),
|
color: colors.outline,
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.close, color: Color(0xFF666666), size: 16),
|
child: Icon(Icons.close, color: colors.onSurface.withValues(alpha: 0.6), size: 16),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFFF8F8F8),
|
fillColor: colors.surfaceContainerHigh,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderSide: BorderSide.none,
|
borderSide: BorderSide.none,
|
||||||
@@ -166,7 +168,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1.5),
|
borderSide: BorderSide(color: colors.primary, width: 1.5),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
),
|
),
|
||||||
@@ -205,26 +207,27 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTypeChip(String label, IconData icon, bool selected, ValueChanged<bool> onChanged) {
|
Widget _buildTypeChip(String label, IconData icon, bool selected, ValueChanged<bool> onChanged) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => onChanged(!selected),
|
onTap: () => onChanged(!selected),
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5),
|
color: selected ? colors.primary : colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 14, color: selected ? Colors.white : const Color(0xFF888888)),
|
Icon(icon, size: 14, color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.5)),
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: selected ? Colors.white : const Color(0xFF888888),
|
color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -234,6 +237,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInitialState() {
|
Widget _buildInitialState() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -242,21 +246,22 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
width: 88,
|
width: 88,
|
||||||
height: 88,
|
height: 88,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.search_rounded, size: 44, color: Color(0xFFD0D0D0)),
|
child: Icon(Icons.search_rounded, size: 44, color: colors.onSurface.withValues(alpha: 0.2)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const Text('输入关键词搜索', style: TextStyle(fontSize: 15, color: Color(0xFF999999))),
|
Text('输入关键词搜索', style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
const Text('可同时筛选影视、书籍、笔记', style: TextStyle(fontSize: 13, color: Color(0xFFCCCCCC))),
|
Text('可同时筛选影视、书籍、笔记', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState() {
|
Widget _buildEmptyState() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -265,15 +270,15 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
width: 88,
|
width: 88,
|
||||||
height: 88,
|
height: 88,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.search_off_rounded, size: 44, color: Color(0xFFD0D0D0)),
|
child: Icon(Icons.search_off_rounded, size: 44, color: colors.onSurface.withValues(alpha: 0.2)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const Text('未找到相关内容', style: TextStyle(fontSize: 15, color: Color(0xFF999999))),
|
Text('未找到相关内容', style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
const Text('换个关键词试试', style: TextStyle(fontSize: 13, color: Color(0xFFCCCCCC))),
|
Text('换个关键词试试', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -302,13 +307,14 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
// ─── 影视结果项 ──────────────────────────────────────────────────────
|
// ─── 影视结果项 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildMovieItem(Movie movie) {
|
Widget _buildMovieItem(Movie movie) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => MovieDetailPage(movie: movie))),
|
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => MovieDetailPage(movie: movie))),
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -328,17 +334,17 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(movie.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
Text(movie.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
if (movie.alternateTitles.isNotEmpty) ...[
|
if (movie.alternateTitles.isNotEmpty) ...[
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text(movie.alternateTitles.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis,
|
Text(movie.alternateTitles.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
|
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -348,13 +354,14 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
// ─── 书籍结果项 ──────────────────────────────────────────────────────
|
// ─── 书籍结果项 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildBookItem(Book book) {
|
Widget _buildBookItem(Book book) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => BookDetailPage(book: book))),
|
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => BookDetailPage(book: book))),
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -374,17 +381,17 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(book.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
Text(book.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
if (book.authors.isNotEmpty) ...[
|
if (book.authors.isNotEmpty) ...[
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text(book.authors.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis,
|
Text(book.authors.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
|
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -394,13 +401,14 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
// ─── 笔记结果项 ──────────────────────────────────────────────────────
|
// ─── 笔记结果项 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildNoteItem(Note note) {
|
Widget _buildNoteItem(Note note) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => NoteDetailPage(note: note))),
|
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => NoteDetailPage(note: note))),
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -410,7 +418,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
children: [
|
children: [
|
||||||
_typeBadge('笔记', const Color(0xFF66BB6A)),
|
_typeBadge('笔记', const Color(0xFF66BB6A)),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
const Icon(Icons.chevron_right, color: Color(0xFFD0D0D0), size: 20),
|
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
@@ -418,7 +426,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
note.summary.trim().isEmpty ? '(无内容)' : note.summary.trim(),
|
note.summary.trim().isEmpty ? '(无内容)' : note.summary.trim(),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 14, color: Color(0xFF333333), height: 1.6),
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75), height: 1.6),
|
||||||
),
|
),
|
||||||
if (note.tags.isNotEmpty) ...[
|
if (note.tags.isNotEmpty) ...[
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
@@ -428,10 +436,10 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
children: note.tags.map((tag) => Container(
|
children: note.tags.map((tag) => Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Text(tag, style: const TextStyle(fontSize: 11, color: Color(0xFF888888))),
|
child: Text(tag, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||||
)).toList(),
|
)).toList(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -444,18 +452,19 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
// ─── 通用组件 ────────────────────────────────────────────────────────
|
// ─── 通用组件 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildPosterThumb(String? path, IconData fallback) {
|
Widget _buildPosterThumb(String? path, IconData fallback) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 64,
|
height: 64,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF0F0F0),
|
color: colors.outlineVariant,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: path != null && path.isNotEmpty
|
child: path != null && path.isNotEmpty
|
||||||
? Image.file(File(path), fit: BoxFit.cover,
|
? Image.file(File(path), fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) => Icon(fallback, size: 22, color: const Color(0xFFCCCCCC)))
|
errorBuilder: (_, __, ___) => Icon(fallback, size: 22, color: colors.onSurface.withValues(alpha: 0.25)))
|
||||||
: Icon(fallback, size: 22, color: const Color(0xFFCCCCCC)),
|
: Icon(fallback, size: 22, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,11 +480,12 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _statusBadge(String status) {
|
Widget _statusBadge(String status) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final (label, bg, fg) = switch (status) {
|
final (label, bg, fg) = switch (status) {
|
||||||
'watched' => ('已看', const Color(0xFF1A1A1A), Colors.white),
|
'watched' => ('已看', colors.primary, colors.onPrimary),
|
||||||
'watching' => ('在看', const Color(0xFFF0F0F0), const Color(0xFF666666)),
|
'watching' => ('在看', colors.outlineVariant, colors.onSurface.withValues(alpha: 0.6)),
|
||||||
'want_to_watch' => ('想看', const Color(0xFFF5F5F5), const Color(0xFF999999)),
|
'want_to_watch' => ('想看', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.4)),
|
||||||
_ => ('', const Color(0xFFF5F5F5), const Color(0xFFBBBBBB)),
|
_ => ('', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.3)),
|
||||||
};
|
};
|
||||||
if (label.isEmpty) return const SizedBox.shrink();
|
if (label.isEmpty) return const SizedBox.shrink();
|
||||||
return Container(
|
return Container(
|
||||||
@@ -486,11 +496,12 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _bookStatusBadge(String status) {
|
Widget _bookStatusBadge(String status) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final (label, bg, fg) = switch (status) {
|
final (label, bg, fg) = switch (status) {
|
||||||
'read' => ('已读', const Color(0xFF1A1A1A), Colors.white),
|
'read' => ('已读', colors.primary, colors.onPrimary),
|
||||||
'reading' => ('在读', const Color(0xFFF0F0F0), const Color(0xFF666666)),
|
'reading' => ('在读', colors.outlineVariant, colors.onSurface.withValues(alpha: 0.6)),
|
||||||
'want_to_read' => ('想读', const Color(0xFFF5F5F5), const Color(0xFF999999)),
|
'want_to_read' => ('想读', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.4)),
|
||||||
_ => ('', const Color(0xFFF5F5F5), const Color(0xFFBBBBBB)),
|
_ => ('', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.3)),
|
||||||
};
|
};
|
||||||
if (label.isEmpty) return const SizedBox.shrink();
|
if (label.isEmpty) return const SizedBox.shrink();
|
||||||
return Container(
|
return Container(
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(title: const Text('数据统计')),
|
appBar: AppBar(title: const Text('数据统计')),
|
||||||
body: Consumer<AppProvider>(
|
body: Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
@@ -63,6 +64,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
// ─── 总览卡片 ────────────────────────────────────────────────────────
|
// ─── 总览卡片 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildTotalCards(List<Movie> movies, List<Book> books, List<Note> notes) {
|
Widget _buildTotalCards(List<Movie> movies, List<Book> books, List<Note> notes) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final items = <_CardData>[];
|
final items = <_CardData>[];
|
||||||
if (_showMovies) items.add(_CardData('影视', movies.length, Icons.movie_outlined, const Color(0xFF4A90D9)));
|
if (_showMovies) items.add(_CardData('影视', movies.length, Icons.movie_outlined, const Color(0xFF4A90D9)));
|
||||||
if (_showBooks) items.add(_CardData('书籍', books.length, Icons.menu_book_outlined, const Color(0xFF7E57C2)));
|
if (_showBooks) items.add(_CardData('书籍', books.length, Icons.menu_book_outlined, const Color(0xFF7E57C2)));
|
||||||
@@ -83,7 +85,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text('${d.count}', style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700, color: d.color)),
|
Text('${d.count}', style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700, color: d.color)),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(d.label, style: const TextStyle(fontSize: 12, color: Color(0xFF888888))),
|
Text(d.label, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -94,6 +96,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
// ─── 状态分布 ────────────────────────────────────────────────────────
|
// ─── 状态分布 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildStatusSection(String title, List items, String Function(dynamic) getStatus, Map<String, String> labels) {
|
Widget _buildStatusSection(String title, List items, String Function(dynamic) getStatus, Map<String, String> labels) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final active = items.where((i) => !(i is Movie) || !i.isDeleted).toList();
|
final active = items.where((i) => !(i is Movie) || !i.isDeleted).toList();
|
||||||
final total = active.length;
|
final total = active.length;
|
||||||
|
|
||||||
@@ -110,11 +113,11 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(e.key, style: const TextStyle(fontSize: 13, color: Color(0xFF666666))),
|
Text(e.key, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text('$count', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
Text('$count', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text('${(pct * 100).toStringAsFixed(0)}%', style: const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))),
|
Text('${(pct * 100).toStringAsFixed(0)}%', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
@@ -122,8 +125,8 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(3),
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: pct,
|
value: pct,
|
||||||
backgroundColor: const Color(0xFFF0F0F0),
|
backgroundColor: colors.outlineVariant,
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
minHeight: 6,
|
minHeight: 6,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -138,6 +141,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
// ─── 类型/标签分布 ───────────────────────────────────────────────────
|
// ─── 类型/标签分布 ───────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildGenreDistribution(String title, List items) {
|
Widget _buildGenreDistribution(String title, List items) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final genreCounts = <String, int>{};
|
final genreCounts = <String, int>{};
|
||||||
for (final item in items) {
|
for (final item in items) {
|
||||||
for (final genre in (item.genres as List<String>)) {
|
for (final genre in (item.genres as List<String>)) {
|
||||||
@@ -161,17 +165,17 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 60,
|
width: 60,
|
||||||
child: Text(e.key, style: const TextStyle(fontSize: 12, color: Color(0xFF666666)), overflow: TextOverflow.ellipsis),
|
child: Text(e.key, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.6)), overflow: TextOverflow.ellipsis),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
child: LinearProgressIndicator(value: pct, backgroundColor: const Color(0xFFF0F0F0), color: const Color(0xFF1A1A1A), minHeight: 4),
|
child: LinearProgressIndicator(value: pct, backgroundColor: colors.outlineVariant, color: colors.primary, minHeight: 4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('${e.value}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
Text('${e.value}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -181,6 +185,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNoteTagDistribution(String title, List<Note> notes) {
|
Widget _buildNoteTagDistribution(String title, List<Note> notes) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final tagCounts = <String, int>{};
|
final tagCounts = <String, int>{};
|
||||||
for (final note in notes) {
|
for (final note in notes) {
|
||||||
for (final tag in note.tags) {
|
for (final tag in note.tags) {
|
||||||
@@ -204,17 +209,17 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 60,
|
width: 60,
|
||||||
child: Text(e.key, style: const TextStyle(fontSize: 12, color: Color(0xFF666666)), overflow: TextOverflow.ellipsis),
|
child: Text(e.key, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.6)), overflow: TextOverflow.ellipsis),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
child: LinearProgressIndicator(value: pct, backgroundColor: const Color(0xFFF0F0F0), color: const Color(0xFF1A1A1A), minHeight: 4),
|
child: LinearProgressIndicator(value: pct, backgroundColor: colors.outlineVariant, color: colors.primary, minHeight: 4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('${e.value}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
Text('${e.value}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -226,6 +231,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
// ─── 评分分布 ────────────────────────────────────────────────────────
|
// ─── 评分分布 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildRatingDistribution(String title, List<Movie> movies, List<Book> books) {
|
Widget _buildRatingDistribution(String title, List<Movie> movies, List<Book> books) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final allRatings = <double>[];
|
final allRatings = <double>[];
|
||||||
for (final m in movies) {
|
for (final m in movies) {
|
||||||
if (m.rating != null) allRatings.add(m.rating!);
|
if (m.rating != null) allRatings.add(m.rating!);
|
||||||
@@ -255,10 +261,10 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Text('平均评分', style: TextStyle(fontSize: 13, color: Color(0xFF666666))),
|
Text('平均评分', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(avg.toStringAsFixed(1), style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A))),
|
Text(avg.toStringAsFixed(1), style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: colors.onSurface)),
|
||||||
const Text(' / 10', style: TextStyle(fontSize: 13, color: Color(0xFFBBBBBB))),
|
Text(' / 10', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -268,7 +274,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 28,
|
width: 28,
|
||||||
child: Text(ranges[i], style: const TextStyle(fontSize: 11, color: Color(0xFF999999))),
|
child: Text(ranges[i], style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -276,14 +282,14 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: maxCount > 0 ? counts[i] / maxCount : 0.0,
|
value: maxCount > 0 ? counts[i] / maxCount : 0.0,
|
||||||
backgroundColor: const Color(0xFFF0F0F0),
|
backgroundColor: colors.outlineVariant,
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
minHeight: 6,
|
minHeight: 6,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('${counts[i]}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
Text('${counts[i]}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
@@ -295,6 +301,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
// ─── 月度趋势 ────────────────────────────────────────────────────────
|
// ─── 月度趋势 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildMonthlyTrend(List<Movie> movies, List<Book> books, List<Note> notes) {
|
Widget _buildMonthlyTrend(List<Movie> movies, List<Book> books, List<Note> notes) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final months = List.generate(6, (i) {
|
final months = List.generate(6, (i) {
|
||||||
final d = DateTime(now.year, now.month - (5 - i), 1);
|
final d = DateTime(now.year, now.month - (5 - i), 1);
|
||||||
@@ -323,7 +330,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
final maxVal = allValues.isEmpty ? 1 : allValues.reduce((a, b) => a > b ? a : b);
|
final maxVal = allValues.isEmpty ? 1 : allValues.reduce((a, b) => a > b ? a : b);
|
||||||
final safeMax = maxVal == 0 ? 1 : maxVal;
|
final safeMax = maxVal == 0 ? 1 : maxVal;
|
||||||
|
|
||||||
final colors = { '影视': const Color(0xFF4A90D9), '书籍': const Color(0xFF7E57C2), '笔记': const Color(0xFF66BB6A) };
|
final brandColors = { '影视': const Color(0xFF4A90D9), '书籍': const Color(0xFF7E57C2), '笔记': const Color(0xFF66BB6A) };
|
||||||
|
|
||||||
return _buildCard(
|
return _buildCard(
|
||||||
title: '近6月趋势',
|
title: '近6月趋势',
|
||||||
@@ -345,7 +352,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
height: h < 2 && e.value[i] > 0 ? 2 : h,
|
height: h < 2 && e.value[i] > 0 ? 2 : h,
|
||||||
margin: const EdgeInsets.only(top: 1),
|
margin: const EdgeInsets.only(top: 1),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colors[e.key]!.withValues(alpha: 0.7),
|
color: brandColors[e.key]!.withValues(alpha: 0.7),
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -357,12 +364,12 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Divider(height: 1, color: Color(0xFFF0F0F0)),
|
Divider(height: 1, color: colors.outlineVariant),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// 月份标签
|
// 月份标签
|
||||||
Row(
|
Row(
|
||||||
children: months.map((m) => Expanded(
|
children: months.map((m) => Expanded(
|
||||||
child: Text(m, textAlign: TextAlign.center, style: const TextStyle(fontSize: 11, color: Color(0xFFBBBBBB))),
|
child: Text(m, textAlign: TextAlign.center, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
)).toList(),
|
)).toList(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -374,9 +381,9 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Container(width: 8, height: 8, decoration: BoxDecoration(color: colors[k], borderRadius: BorderRadius.circular(2))),
|
Container(width: 8, height: 8, decoration: BoxDecoration(color: brandColors[k], borderRadius: BorderRadius.circular(2))),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(k, style: const TextStyle(fontSize: 12, color: Color(0xFF888888))),
|
Text(k, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)).toList(),
|
)).toList(),
|
||||||
@@ -394,10 +401,11 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
// ─── 通用卡片 ────────────────────────────────────────────────────────
|
// ─── 通用卡片 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildCard({required String title, required Widget child}) {
|
Widget _buildCard({required String title, required Widget child}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(18),
|
padding: const EdgeInsets.all(18),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -405,9 +413,9 @@ class _StatisticsPageState extends State<StatisticsPage> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(width: 3, height: 14, decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(2))),
|
Container(width: 3, height: 14, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(2))),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|||||||
@@ -153,8 +153,9 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('漫步'),
|
title: const Text('漫步'),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -166,15 +167,15 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
|||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(18),
|
borderRadius: BorderRadius.circular(18),
|
||||||
),
|
),
|
||||||
child: const Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.casino_outlined, size: 14, color: Colors.white),
|
Icon(Icons.casino_outlined, size: 14, color: colors.onPrimary),
|
||||||
SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
Text('随机', style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.w500)),
|
Text('随机', style: TextStyle(fontSize: 12, color: colors.onPrimary, fontWeight: FontWeight.w500)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -183,8 +184,10 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: _currentItem == null
|
body: _currentItem == null
|
||||||
? const Center(child: Text('还没有任何内容\n去添加一些吧', textAlign: TextAlign.center,
|
? Center(
|
||||||
style: TextStyle(fontSize: 15, color: Color(0xFFBBBBBB), height: 1.6)))
|
child: Text('还没有任何内容\n去添加一些吧',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.3), height: 1.6)))
|
||||||
: Consumer<AppProvider>(builder: (context, provider, _) {
|
: Consumer<AppProvider>(builder: (context, provider, _) {
|
||||||
final item = _currentItem!;
|
final item = _currentItem!;
|
||||||
final hasImage = item.imagePath != null && item.imagePath!.isNotEmpty && File(item.imagePath!).existsSync();
|
final hasImage = item.imagePath != null && item.imagePath!.isNotEmpty && File(item.imagePath!).existsSync();
|
||||||
@@ -216,7 +219,7 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
|||||||
child: Text(
|
child: Text(
|
||||||
item.title,
|
item.title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A), height: 1.3),
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -225,7 +228,7 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text(item.subtitle, textAlign: TextAlign.center,
|
child: Text(item.subtitle, textAlign: TextAlign.center,
|
||||||
style: const TextStyle(fontSize: 13, color: Color(0xFF999999))),
|
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -242,18 +245,18 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
|||||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(item.detail,
|
child: Text(item.detail,
|
||||||
style: const TextStyle(fontSize: 13, color: Color(0xFF777777), height: 1.7)),
|
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6), height: 1.7)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
// 时间
|
// 时间
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(_actionText(item), style: const TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))),
|
Text(_actionText(item), style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||||
|
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
@@ -302,11 +305,12 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNoteCard(_StrollItem item, bool hasImage) {
|
Widget _buildNoteCard(_StrollItem item, bool hasImage) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
constraints: const BoxConstraints(maxWidth: 360),
|
constraints: const BoxConstraints(maxWidth: 360),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(color: Colors.black.withValues(alpha: 0.06), blurRadius: 16, offset: const Offset(0, 6)),
|
BoxShadow(color: Colors.black.withValues(alpha: 0.06), blurRadius: 16, offset: const Offset(0, 6)),
|
||||||
@@ -322,12 +326,12 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Text(item.detail.isEmpty ? '(无内容)' : item.detail,
|
child: Text(item.detail.isEmpty ? '(无内容)' : item.detail,
|
||||||
style: const TextStyle(fontSize: 14, color: Color(0xFF444444), height: 1.8)),
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.73), height: 1.8)),
|
||||||
),
|
),
|
||||||
const Divider(height: 1, color: Color(0xFFF0F0F0)),
|
Divider(height: 1, color: colors.outlineVariant),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
child: Text('${item.detail.length} 字 · Mooknote', style: const TextStyle(fontSize: 11, color: Color(0xFFBBBBBB))),
|
child: Text('${item.detail.length} 字 · Mooknote', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -335,8 +339,9 @@ class _StrollPageState extends State<StrollPage> with SingleTickerProviderStateM
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPlaceholder(_StrollItem item) {
|
Widget _buildPlaceholder(_StrollItem item) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHigh,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(item.icon, size: 48, color: item.color.withValues(alpha: 0.2)),
|
child: Icon(item.icon, size: 48, color: item.color.withValues(alpha: 0.2)),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -40,71 +40,74 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('本地备份'),
|
title: const Text('本地备份'),
|
||||||
),
|
),
|
||||||
body: _isLoading
|
body: _isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: ListView(
|
: ListView(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
children: [
|
children: [
|
||||||
// 自动备份开关 - 紧凑一行
|
// 自动备份开关 - 紧凑一行
|
||||||
_buildAutoBackupSection(),
|
_buildAutoBackupSection(colors),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// 手动备份
|
// 手动备份
|
||||||
_buildSectionTitle('手动备份'),
|
_buildSectionTitle(colors, '手动备份'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildActionCard(
|
_buildActionCard(
|
||||||
title: '导出数据',
|
colors: colors,
|
||||||
description: '将所有数据导出为 zip 文件,可用于备份或迁移到其他设备',
|
title: '导出数据',
|
||||||
icon: Icons.upload_outlined,
|
description: '将所有数据导出为 zip 文件,可用于备份或迁移到其他设备',
|
||||||
buttonText: '导出',
|
icon: Icons.upload_outlined,
|
||||||
isLoading: _isExporting,
|
buttonText: '导出',
|
||||||
onTap: _exportData,
|
isLoading: _isExporting,
|
||||||
),
|
onTap: _exportData,
|
||||||
const SizedBox(height: 12),
|
),
|
||||||
_buildActionCard(
|
const SizedBox(height: 12),
|
||||||
title: '导入数据',
|
_buildActionCard(
|
||||||
description: '从备份文件导入数据,将覆盖当前所有数据',
|
colors: colors,
|
||||||
icon: Icons.download_outlined,
|
title: '导入数据',
|
||||||
buttonText: '导入',
|
description: '从备份文件导入数据,将覆盖当前所有数据',
|
||||||
isLoading: _isImporting,
|
icon: Icons.download_outlined,
|
||||||
onTap: _importData,
|
buttonText: '导入',
|
||||||
isDestructive: true,
|
isLoading: _isImporting,
|
||||||
),
|
onTap: _importData,
|
||||||
|
isDestructive: true,
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
// 使用说明
|
// 使用说明
|
||||||
_buildInfoSection(),
|
_buildInfoSection(colors),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建区块标题
|
/// 构建区块标题
|
||||||
Widget _buildSectionTitle(String title) {
|
Widget _buildSectionTitle(ColorScheme colors, String title) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 4,
|
width: 4,
|
||||||
height: 16,
|
height: 16,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -113,6 +116,7 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
|
|
||||||
/// 构建操作卡片
|
/// 构建操作卡片
|
||||||
Widget _buildActionCard({
|
Widget _buildActionCard({
|
||||||
|
required ColorScheme colors,
|
||||||
required String title,
|
required String title,
|
||||||
required String description,
|
required String description,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
@@ -124,7 +128,7 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -136,19 +140,17 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
width: 44,
|
width: 44,
|
||||||
height: 44,
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isDestructive
|
color: isDestructive ? Colors.red.withOpacity(0.3) : colors.outline,
|
||||||
? Colors.red.withOpacity(0.3)
|
|
||||||
: const Color(0xFFE8E8E8),
|
|
||||||
width: 0.5,
|
width: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 22,
|
size: 22,
|
||||||
color: isDestructive ? Colors.red : const Color(0xFF666666),
|
color: isDestructive ? Colors.red : colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
@@ -158,18 +160,18 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
description,
|
description,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -185,25 +187,25 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isLoading ? const Color(0xFFCCCCCC) : const Color(0xFF1A1A1A),
|
color: isLoading ? colors.onSurface.withValues(alpha: 0.25) : colors.primary,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: isLoading
|
child: isLoading
|
||||||
? const SizedBox(
|
? SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
valueColor: AlwaysStoppedAnimation(Colors.white),
|
valueColor: AlwaysStoppedAnimation(colors.onPrimary),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
buttonText,
|
buttonText,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Colors.white,
|
color: colors.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -215,11 +217,11 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 构建信息说明区域
|
/// 构建信息说明区域
|
||||||
Widget _buildInfoSection() {
|
Widget _buildInfoSection(ColorScheme colors) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -231,42 +233,42 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.info_outline,
|
Icons.info_outline,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
const Text(
|
Text(
|
||||||
'使用说明',
|
'使用说明',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildInfoItem('1', '导出数据会生成一个 .zip 文件,包含所有数据和图片'),
|
_buildInfoItem(colors, '1', '导出数据会生成一个 .zip 文件,包含所有数据和图片'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildInfoItem('2', '选择保存路径后,可以通过微信、邮件等方式发送备份文件'),
|
_buildInfoItem(colors, '2', '选择保存路径后,可以通过微信、邮件等方式发送备份文件'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildInfoItem('3', '在新设备上选择导入数据,选择备份文件即可恢复'),
|
_buildInfoItem(colors, '3', '在新设备上选择导入数据,选择备份文件即可恢复'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildInfoItem('4', '导入数据会完全覆盖当前设备的数据,请谨慎操作'),
|
_buildInfoItem(colors, '4', '导入数据会完全覆盖当前设备的数据,请谨慎操作'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建信息项
|
/// 构建信息项
|
||||||
Widget _buildInfoItem(String number, String text) {
|
Widget _buildInfoItem(ColorScheme colors, String number, String text) {
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -274,16 +276,16 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFE8E8E8),
|
color: colors.outline,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
number,
|
number,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -292,9 +294,9 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -311,85 +313,93 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
}) {
|
}) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) {
|
||||||
backgroundColor: Colors.white,
|
final colors = Theme.of(ctx).colorScheme;
|
||||||
elevation: 0,
|
return AlertDialog(
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
backgroundColor: colors.surface,
|
||||||
title: Column(
|
elevation: 0,
|
||||||
children: [
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
Container(
|
title: Column(
|
||||||
width: 48,
|
children: [
|
||||||
height: 48,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFF5F5F5),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 24),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(title, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
content,
|
|
||||||
style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
if (detail != null && detail.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: 48,
|
||||||
padding: const EdgeInsets.all(12),
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
detail,
|
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
|
|
||||||
maxLines: 3,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
|
child: Icon(Icons.check, color: colors.primary, size: 24),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
],
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
|
||||||
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(ctx),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
minimumSize: const Size(120, 40),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
),
|
|
||||||
child: const Text('确定', style: TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))),
|
|
||||||
),
|
),
|
||||||
],
|
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||||
),
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
content,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
if (detail != null && detail.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
detail,
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
||||||
|
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
minimumSize: const Size(120, 40),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
),
|
||||||
|
child: Text('确定', style: TextStyle(fontSize: 14, color: colors.primary)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 导出数据
|
/// 导出数据
|
||||||
Future<void> _exportData() async {
|
Future<void> _exportData() async {
|
||||||
setState(() => _isExporting = true);
|
setState(() => _isExporting = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = await BackupService.instance.exportDataWithImages();
|
final result = await BackupService.instance.exportDataWithImages();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
if (result.cancelled) {
|
if (result.cancelled) {
|
||||||
ToastUtil.show(context, '已取消导出');
|
ToastUtil.show(context, '已取消导出');
|
||||||
} else if (result.success) {
|
} else if (result.success) {
|
||||||
_showSuccessDialog(
|
_showSuccessDialog(
|
||||||
title: '导出成功',
|
title: '导出成功',
|
||||||
content: '备份文件已保存,包含:\n影视 ${result.movieCount} · 书籍 ${result.bookCount} · 笔记 ${result.noteCount} · 图片 ${result.imageCount}',
|
content:
|
||||||
|
'备份文件已保存,包含:\n影视 ${result.movieCount} · 书籍 ${result.bookCount} · 笔记 ${result.noteCount} · 图片 ${result.imageCount}',
|
||||||
detail: result.filePath ?? '',
|
detail: result.filePath ?? '',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -411,54 +421,63 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
// 显示确认对话框
|
// 显示确认对话框
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (ctx) {
|
||||||
backgroundColor: Colors.white,
|
final colors = Theme.of(ctx).colorScheme;
|
||||||
elevation: 0,
|
return AlertDialog(
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
backgroundColor: colors.surface,
|
||||||
title: Row(
|
elevation: 0,
|
||||||
children: [
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
Container(
|
title: Row(
|
||||||
width: 40,
|
children: [
|
||||||
height: 40,
|
Container(
|
||||||
decoration: BoxDecoration(
|
width: 40,
|
||||||
color: Colors.red.withOpacity(0.08),
|
height: 40,
|
||||||
borderRadius: BorderRadius.circular(10),
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.withOpacity(0.08),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
|
const SizedBox(width: 12),
|
||||||
|
Text('确认导入',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||||
|
content: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16),
|
||||||
|
child: Text(
|
||||||
|
'导入数据将覆盖当前所有数据,此操作不可恢复。',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
||||||
|
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
),
|
||||||
|
child: Text('取消',
|
||||||
|
style: TextStyle(
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.6), fontSize: 14)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
),
|
||||||
|
child: const Text('确认导入',
|
||||||
|
style: TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.w600)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
|
||||||
const Text('确认导入', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
},
|
||||||
content: const Padding(
|
|
||||||
padding: EdgeInsets.only(top: 16),
|
|
||||||
child: Text(
|
|
||||||
'导入数据将覆盖当前所有数据,此操作不可恢复。',
|
|
||||||
style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
|
||||||
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, false),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
),
|
|
||||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666), fontSize: 14)),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
),
|
|
||||||
child: const Text('确认导入', style: TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.w600)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed != true) return;
|
if (confirmed != true) return;
|
||||||
@@ -467,7 +486,7 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final result = await BackupService.instance.importData();
|
final result = await BackupService.instance.importData();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
if (result.cancelled) {
|
if (result.cancelled) {
|
||||||
@@ -499,13 +518,13 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 构建自动备份区域 - 紧凑一行
|
/// 构建自动备份区域 - 紧凑一行
|
||||||
Widget _buildAutoBackupSection() {
|
Widget _buildAutoBackupSection(ColorScheme colors) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -513,11 +532,12 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.schedule, size: 20, color: Color(0xFF666666)),
|
child: Icon(Icons.schedule,
|
||||||
|
size: 20, color: colors.onSurface.withValues(alpha: 0.6)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -525,22 +545,25 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
? Column(
|
? Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'自动本地备份',
|
'自动本地备份',
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
style: TextStyle(
|
||||||
|
fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
_backupDirPath!,
|
_backupDirPath!,
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
|
style:
|
||||||
|
TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: const Text(
|
: Text(
|
||||||
'自动本地备份',
|
'自动本地备份',
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
style: TextStyle(
|
||||||
|
fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Switch(
|
Switch(
|
||||||
@@ -555,14 +578,13 @@ class _BackupPageState extends State<BackupPage> {
|
|||||||
}
|
}
|
||||||
await _loadAutoBackupStatus();
|
await _loadAutoBackupStatus();
|
||||||
},
|
},
|
||||||
activeColor: const Color(0xFF1A1A1A),
|
activeThumbColor: colors.primary,
|
||||||
activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3),
|
activeTrackColor: colors.primary.withValues(alpha: 0.3),
|
||||||
inactiveThumbColor: Colors.white,
|
inactiveThumbColor: colors.surface,
|
||||||
inactiveTrackColor: const Color(0xFFE5E5E5),
|
inactiveTrackColor: colors.outline,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,95 +8,163 @@ class CloudSyncPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF8F8F8),
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
appBar: AppBar(title: const Text('云备份')),
|
appBar: AppBar(title: const Text('云备份')),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
children: [
|
children: [
|
||||||
_buildSectionTitle('选择备份方式'),
|
_buildSectionTitle(colors, '选择备份方式'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildOption(
|
_buildOption(
|
||||||
|
colors: colors,
|
||||||
icon: Icons.storage_outlined,
|
icon: Icons.storage_outlined,
|
||||||
title: 'WebDAV 备份',
|
title: 'WebDAV 备份',
|
||||||
subtitle: '通过 WebDAV 协议备份到个人云盘',
|
subtitle: '通过 WebDAV 协议备份到个人云盘',
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())),
|
onTap: () =>
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildOption(
|
_buildOption(
|
||||||
|
colors: colors,
|
||||||
icon: Icons.sync_outlined,
|
icon: Icons.sync_outlined,
|
||||||
title: '服务端实时同步',
|
title: '服务端实时同步',
|
||||||
subtitle: '自建服务端,多设备数据实时同步',
|
subtitle: '自建服务端,多设备数据实时同步',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ServerSyncPage())),
|
onTap: () =>
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (_) => const ServerSyncPage())),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
_buildInfo(),
|
_buildInfo(colors),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSectionTitle(String title) {
|
Widget _buildSectionTitle(ColorScheme colors, String title) {
|
||||||
return Row(children: [
|
return Row(children: [
|
||||||
Container(width: 3, height: 14, decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(2))),
|
Container(
|
||||||
|
width: 3,
|
||||||
|
height: 14,
|
||||||
|
decoration:
|
||||||
|
BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(2))),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
Text(title,
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOption({required IconData icon, required String title, required String subtitle, required VoidCallback onTap, bool enabled = true}) {
|
Widget _buildOption({
|
||||||
|
required ColorScheme colors,
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required String subtitle,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
bool enabled = true,
|
||||||
|
}) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: enabled ? onTap : null,
|
onTap: enabled ? onTap : null,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(18),
|
padding: const EdgeInsets.all(18),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: enabled ? Colors.white : const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(14),
|
color: enabled ? colors.surface : colors.surfaceContainerHighest,
|
||||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))],
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.03),
|
||||||
|
blurRadius: 6,
|
||||||
|
offset: const Offset(0, 2)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: enabled ? const Color(0xFF666666) : const Color(0xFFBBBBBB), size: 22)),
|
Container(
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: Icon(icon,
|
||||||
|
color: enabled
|
||||||
|
? colors.onSurface.withValues(alpha: 0.6)
|
||||||
|
: colors.onSurface.withValues(alpha: 0.3),
|
||||||
|
size: 22)),
|
||||||
const SizedBox(width: 14),
|
const SizedBox(width: 14),
|
||||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
Expanded(
|
||||||
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: enabled ? const Color(0xFF1A1A1A) : const Color(0xFFBBBBBB))),
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: enabled ? colors.onSurface : colors.onSurface.withValues(alpha: 0.3))),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text(subtitle, style: TextStyle(fontSize: 12, color: enabled ? const Color(0xFF999999) : const Color(0xFFCCCCCC))),
|
Text(subtitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: enabled
|
||||||
|
? colors.onSurface.withValues(alpha: 0.4)
|
||||||
|
: colors.onSurface.withValues(alpha: 0.25))),
|
||||||
])),
|
])),
|
||||||
Icon(Icons.chevron_right, color: enabled ? const Color(0xFFCCCCCC) : const Color(0xFFE5E5E5)),
|
Icon(Icons.chevron_right,
|
||||||
|
color: enabled
|
||||||
|
? colors.onSurface.withValues(alpha: 0.25)
|
||||||
|
: colors.outline),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInfo() {
|
Widget _buildInfo(ColorScheme colors) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(18),
|
padding: const EdgeInsets.all(18),
|
||||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14),
|
decoration: BoxDecoration(
|
||||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))],
|
color: colors.surface,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.03),
|
||||||
|
blurRadius: 6,
|
||||||
|
offset: const Offset(0, 2)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
Container(width: 36, height: 36, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(8)), child: const Icon(Icons.info_outline, size: 18, color: Color(0xFF666666))),
|
Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: Icon(Icons.info_outline,
|
||||||
|
size: 18, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
const Text('使用说明', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
Text('使用说明',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
]),
|
]),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_infoItem('WebDAV 备份:将数据备份到支持 WebDAV 的云盘'),
|
_infoItem(colors, 'WebDAV 备份:将数据备份到支持 WebDAV 的云盘'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_infoItem('服务端实时同步:通过自建服务端实现多设备实时同步'),
|
_infoItem(colors, '服务端实时同步:通过自建服务端实现多设备实时同步'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_infoItem('激活码由服务端管理员在管理后台生成'),
|
_infoItem(colors, '激活码由服务端管理员在管理后台生成'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_infoItem('建议定期备份 + 实时同步配合使用'),
|
_infoItem(colors, '建议定期备份 + 实时同步配合使用'),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _infoItem(String text) {
|
Widget _infoItem(ColorScheme colors, String text) {
|
||||||
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Container(width: 5, height: 5, margin: const EdgeInsets.only(top: 5), decoration: BoxDecoration(color: const Color(0xFFBBBBBB), shape: BoxShape.circle)),
|
Container(
|
||||||
|
width: 5,
|
||||||
|
height: 5,
|
||||||
|
margin: const EdgeInsets.only(top: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.3), shape: BoxShape.circle)),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(child: Text(text, style: const TextStyle(fontSize: 12, color: Color(0xFF888888), height: 1.5))),
|
Expanded(
|
||||||
|
child: Text(text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5), height: 1.5))),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,16 +157,24 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
|
|||||||
Future<void> _disconnect() async {
|
Future<void> _disconnect() async {
|
||||||
final confirm = await showDialog<bool>(
|
final confirm = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) {
|
||||||
backgroundColor: Colors.white,
|
final colors = Theme.of(ctx).colorScheme;
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
return AlertDialog(
|
||||||
title: const Text('断开连接', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
backgroundColor: colors.surface,
|
||||||
content: const Text('将清除服务器配置和激活信息,确定要断开吗?', style: TextStyle(fontSize: 14, color: Color(0xFF666666))),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
actions: [
|
title: const Text('断开连接', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))),
|
content: Text('将清除服务器配置和激活信息,确定要断开吗?',
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定', style: TextStyle(color: Color(0xFFE53935)))),
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
],
|
actions: [
|
||||||
),
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('确定', style: TextStyle(color: Color(0xFFE53935)))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (confirm != true) return;
|
if (confirm != true) return;
|
||||||
|
|
||||||
@@ -190,63 +198,107 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF8F8F8),
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
appBar: AppBar(title: const Text('服务端实时同步')),
|
appBar: AppBar(title: const Text('服务端实时同步')),
|
||||||
body: ListView(padding: const EdgeInsets.all(20), children: [
|
body: ListView(padding: const EdgeInsets.all(20), children: [
|
||||||
// 状态卡片
|
// 状态卡片
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16),
|
decoration: BoxDecoration(
|
||||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))],
|
color: colors.surface,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
Container(width: 10, height: 10, decoration: BoxDecoration(
|
Container(
|
||||||
color: _isActivated ? const Color(0xFF66BB6A) : const Color(0xFFDDDDDD), shape: BoxShape.circle)),
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _isActivated
|
||||||
|
? const Color(0xFF66BB6A)
|
||||||
|
: colors.onSurface.withValues(alpha: 0.15),
|
||||||
|
shape: BoxShape.circle)),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(_isActivated ? '已激活' : '未激活',
|
Text(_isActivated ? '已激活' : '未激活',
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600,
|
style: TextStyle(
|
||||||
color: _isActivated ? const Color(0xFF66BB6A) : const Color(0xFFBBBBBB))),
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: _isActivated
|
||||||
|
? const Color(0xFF66BB6A)
|
||||||
|
: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (_isActivated)
|
if (_isActivated)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _disconnect,
|
onTap: _disconnect,
|
||||||
child: Container(padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
child: Container(
|
||||||
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(6)),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||||
child: const Text('断开', style: TextStyle(fontSize: 12, color: Color(0xFFE57373)))),
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(6)),
|
||||||
|
child: const Text('断开',
|
||||||
|
style: TextStyle(fontSize: 12, color: Color(0xFFE57373)))),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Text('服务器地址', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
Text('服务器地址',
|
||||||
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
TextField(controller: _urlController, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
TextField(
|
||||||
decoration: _inputDeco('例: http://192.168.1.100:5000')),
|
controller: _urlController,
|
||||||
|
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||||
|
decoration: _inputDeco(colors, '例: http://192.168.1.100:5000'),
|
||||||
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
const Text('激活码', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
Text('激活码',
|
||||||
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
TextField(controller: _codeController, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
|
TextField(
|
||||||
textCapitalization: TextCapitalization.characters, decoration: _inputDeco('例: MK-A1B2C3D4E5F6')),
|
controller: _codeController,
|
||||||
|
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
decoration: _inputDeco(colors, '例: MK-A1B2C3D4E5F6')),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
SizedBox(width: double.infinity,
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _isChecking ? null : _checkActivation,
|
onPressed: _isChecking ? null : _checkActivation,
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF1A1A1A), foregroundColor: Colors.white,
|
style: ElevatedButton.styleFrom(
|
||||||
disabledBackgroundColor: const Color(0xFFDDDDDD), elevation: 0,
|
backgroundColor: colors.primary,
|
||||||
|
foregroundColor: colors.onPrimary,
|
||||||
|
disabledBackgroundColor: colors.onSurface.withValues(alpha: 0.15),
|
||||||
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 13)),
|
padding: const EdgeInsets.symmetric(vertical: 13)),
|
||||||
child: _isChecking
|
child: _isChecking
|
||||||
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
? SizedBox(
|
||||||
: Text(_isActivated ? '重新验证' : '验证激活', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2, color: colors.onPrimary))
|
||||||
|
: Text(_isActivated ? '重新验证' : '验证激活',
|
||||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_expiresText.isNotEmpty) ...[
|
if (_expiresText.isNotEmpty) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Center(child: Row(mainAxisSize: MainAxisSize.min, children: [
|
Center(
|
||||||
Icon(Icons.access_time, size: 14, color: _prefs.syncIsPermanent ? const Color(0xFF66BB6A) : const Color(0xFFFF9800)),
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
Icon(Icons.access_time,
|
||||||
|
size: 14,
|
||||||
|
color: _prefs.syncIsPermanent
|
||||||
|
? const Color(0xFF66BB6A)
|
||||||
|
: const Color(0xFFFF9800)),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(_expiresText, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500,
|
Text(_expiresText,
|
||||||
color: _prefs.syncIsPermanent ? const Color(0xFF66BB6A) : const Color(0xFFFF9800))),
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: _prefs.syncIsPermanent
|
||||||
|
? const Color(0xFF66BB6A)
|
||||||
|
: const Color(0xFFFF9800))),
|
||||||
])),
|
])),
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
@@ -255,47 +307,78 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
|
|||||||
// 同步开关
|
// 同步开关
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16),
|
decoration: BoxDecoration(
|
||||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))]),
|
color: colors.surface,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))
|
||||||
|
]),
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)),
|
Container(
|
||||||
child: Icon(_syncEnabled ? Icons.sync : Icons.sync_disabled,
|
width: 44,
|
||||||
color: _syncEnabled ? const Color(0xFF1A1A1A) : const Color(0xFFCCCCCC), size: 22)),
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: Icon(_syncEnabled ? Icons.sync : Icons.sync_disabled,
|
||||||
|
color: _syncEnabled
|
||||||
|
? colors.primary
|
||||||
|
: colors.onSurface.withValues(alpha: 0.25),
|
||||||
|
size: 22)),
|
||||||
const SizedBox(width: 14),
|
const SizedBox(width: 14),
|
||||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
Expanded(
|
||||||
const Text('服务端实时同步', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text('服务端实时同步',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(_syncEnabled ? '使用服务端数据,多设备实时共享' : '关闭后下载数据到本地使用',
|
Text(_syncEnabled ? '使用服务端数据,多设备实时共享' : '关闭后下载数据到本地使用',
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))),
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
])),
|
])),
|
||||||
Switch(value: _syncEnabled, onChanged: _isActivated ? _toggleSync : null,
|
Switch(
|
||||||
activeColor: const Color(0xFF1A1A1A), activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3),
|
value: _syncEnabled,
|
||||||
inactiveThumbColor: Colors.white, inactiveTrackColor: const Color(0xFFE5E5E5)),
|
onChanged: _isActivated ? _toggleSync : null,
|
||||||
|
activeThumbColor: colors.primary,
|
||||||
|
activeTrackColor: colors.primary.withValues(alpha: 0.3),
|
||||||
|
inactiveThumbColor: colors.surface,
|
||||||
|
inactiveTrackColor: colors.outline),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// 说明
|
// 说明
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(18),
|
padding: const EdgeInsets.all(18),
|
||||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14),
|
decoration: BoxDecoration(
|
||||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))]),
|
color: colors.surface,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))
|
||||||
|
]),
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
Container(width: 36, height: 36, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(8)),
|
Container(
|
||||||
child: const Icon(Icons.info_outline, size: 18, color: Color(0xFF666666))),
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: Icon(Icons.info_outline,
|
||||||
|
size: 18, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
const Text('使用说明', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
Text('使用说明',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
]),
|
]),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_infoItem('1. 在服务端管理后台生成激活码'),
|
_infoItem(colors, '1. 在服务端管理后台生成激活码'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_infoItem('2. 输入服务器地址和激活码完成验证'),
|
_infoItem(colors, '2. 输入服务器地址和激活码完成验证'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_infoItem('3. 验证通过后自动开启实时同步'),
|
_infoItem(colors, '3. 验证通过后自动开启实时同步'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_infoItem('4. 开启时所有数据通过服务端接口操作'),
|
_infoItem(colors, '4. 开启时所有数据通过服务端接口操作'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_infoItem('5. 关闭时从服务端下载数据到本地使用'),
|
_infoItem(colors, '5. 关闭时从服务端下载数据到本地使用'),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
@@ -303,21 +386,33 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
InputDecoration _inputDeco(String hint) {
|
InputDecoration _inputDeco(ColorScheme colors, String hint) {
|
||||||
return InputDecoration(
|
return InputDecoration(
|
||||||
hintText: hint, hintStyle: const TextStyle(fontSize: 13, color: Color(0xFFCCCCCC)),
|
hintText: hint,
|
||||||
filled: true, fillColor: const Color(0xFFF8F8F8),
|
hintStyle: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
|
filled: true,
|
||||||
|
fillColor: colors.surfaceContainerHigh,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none),
|
||||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1)),
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: colors.primary, width: 1)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _infoItem(String text) {
|
Widget _infoItem(ColorScheme colors, String text) {
|
||||||
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Container(width: 5, height: 5, margin: const EdgeInsets.only(top: 6), decoration: BoxDecoration(color: const Color(0xFFBBBBBB), shape: BoxShape.circle)),
|
Container(
|
||||||
|
width: 5,
|
||||||
|
height: 5,
|
||||||
|
margin: const EdgeInsets.only(top: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.3), shape: BoxShape.circle)),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(child: Text(text, style: const TextStyle(fontSize: 13, color: Color(0xFF888888), height: 1.5))),
|
Expanded(
|
||||||
|
child: Text(text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5), height: 1.5))),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,22 +66,37 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
final password = _passwordController.text;
|
final password = _passwordController.text;
|
||||||
final path = _pathController.text.trim();
|
final path = _pathController.text.trim();
|
||||||
|
|
||||||
if (url.isEmpty) { ToastUtil.show(context, '请输入服务器地址'); return; }
|
if (url.isEmpty) {
|
||||||
if (username.isEmpty) { ToastUtil.show(context, '请输入用户名'); return; }
|
ToastUtil.show(context, '请输入服务器地址');
|
||||||
if (password.isEmpty) { ToastUtil.show(context, '请输入密码'); return; }
|
return;
|
||||||
|
}
|
||||||
|
if (username.isEmpty) {
|
||||||
|
ToastUtil.show(context, '请输入用户名');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password.isEmpty) {
|
||||||
|
ToastUtil.show(context, '请输入密码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = await WebDAVService.instance.testConnection(
|
final result = await WebDAVService.instance.testConnection(
|
||||||
url: url, username: username, password: password, path: path,
|
url: url,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
path: path,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
if (result['success'] == true) {
|
if (result['success'] == true) {
|
||||||
await WebDAVService.instance.saveConfig(
|
await WebDAVService.instance.saveConfig(
|
||||||
url: url, username: username, password: password, path: path,
|
url: url,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
path: path,
|
||||||
);
|
);
|
||||||
setState(() => _isConfigured = true);
|
setState(() => _isConfigured = true);
|
||||||
ToastUtil.show(context, result['message'] ?? '连接成功,配置已保存');
|
ToastUtil.show(context, result['message'] ?? '连接成功,配置已保存');
|
||||||
@@ -104,7 +119,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
final details = '上传: ${result.uploadedFiles} 文件, ${result.uploadedImages} 图片\n'
|
final details = '上传: ${result.uploadedFiles} 文件, ${result.uploadedImages} 图片\n'
|
||||||
'下载: ${result.downloadedFiles} 文件, ${result.downloadedImages} 图片';
|
'下载: ${result.downloadedFiles} 文件, ${result.downloadedImages} 图片';
|
||||||
|
|
||||||
if (result.needReload) {
|
if (result.needReload) {
|
||||||
ToastUtil.show(context, '数据已更新,正在重新加载...');
|
ToastUtil.show(context, '数据已更新,正在重新加载...');
|
||||||
@@ -129,39 +144,48 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
void _showResultDialog(String title, String content) {
|
void _showResultDialog(String title, String content) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) {
|
||||||
backgroundColor: Colors.white,
|
final colors = Theme.of(ctx).colorScheme;
|
||||||
elevation: 0,
|
return AlertDialog(
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
backgroundColor: colors.surface,
|
||||||
title: Column(
|
elevation: 0,
|
||||||
children: [
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
Container(
|
title: Column(
|
||||||
width: 48, height: 48,
|
children: [
|
||||||
decoration: BoxDecoration(
|
Container(
|
||||||
color: const Color(0xFFF5F5F5),
|
width: 48,
|
||||||
borderRadius: BorderRadius.circular(12),
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.check, color: colors.primary, size: 24),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 24),
|
const SizedBox(height: 16),
|
||||||
),
|
Text(title,
|
||||||
const SizedBox(height: 16),
|
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
Text(title, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
|
||||||
content: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 12),
|
|
||||||
child: Text(content, style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6), textAlign: TextAlign.center),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
|
||||||
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(ctx),
|
|
||||||
style: TextButton.styleFrom(minimumSize: const Size(120, 40), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
|
||||||
child: const Text('确定', style: TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))),
|
|
||||||
),
|
),
|
||||||
],
|
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||||
),
|
content: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
child: Text(content,
|
||||||
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6),
|
||||||
|
textAlign: TextAlign.center),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
||||||
|
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
minimumSize: const Size(120, 40),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||||
|
child: Text('确定', style: TextStyle(fontSize: 14, color: colors.primary)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,25 +212,40 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
final selected = await showModalBottomSheet<int>(
|
final selected = await showModalBottomSheet<int>(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
shape: const RoundedRectangleBorder(
|
||||||
builder: (ctx) => SafeArea(
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||||
child: Column(
|
builder: (ctx) {
|
||||||
mainAxisSize: MainAxisSize.min,
|
final colors = Theme.of(ctx).colorScheme;
|
||||||
children: [
|
return SafeArea(
|
||||||
const SizedBox(height: 8),
|
child: Column(
|
||||||
Container(width: 36, height: 4, decoration: BoxDecoration(color: const Color(0xFFDDDDDD), borderRadius: BorderRadius.circular(2))),
|
mainAxisSize: MainAxisSize.min,
|
||||||
const SizedBox(height: 12),
|
children: [
|
||||||
const Text('同步间隔', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
const SizedBox(height: 8),
|
||||||
const SizedBox(height: 8),
|
Container(
|
||||||
...intervals.map((i) => ListTile(
|
width: 36,
|
||||||
title: Text('$i 分钟', style: TextStyle(fontSize: 15, fontWeight: _autoSyncInterval == i ? FontWeight.w600 : FontWeight.w400, color: const Color(0xFF1A1A1A))),
|
height: 4,
|
||||||
trailing: _autoSyncInterval == i ? const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 20) : null,
|
decoration: BoxDecoration(
|
||||||
onTap: () => Navigator.pop(ctx, i),
|
color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
|
||||||
)),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 8),
|
Text('同步间隔',
|
||||||
],
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
),
|
const SizedBox(height: 8),
|
||||||
),
|
...intervals.map((i) => ListTile(
|
||||||
|
title: Text('$i 分钟',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: _autoSyncInterval == i ? FontWeight.w600 : FontWeight.w400,
|
||||||
|
color: colors.onSurface)),
|
||||||
|
trailing: _autoSyncInterval == i
|
||||||
|
? Icon(Icons.check, color: colors.primary, size: 20)
|
||||||
|
: null,
|
||||||
|
onTap: () => Navigator.pop(ctx, i),
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selected != null && selected != _autoSyncInterval) {
|
if (selected != null && selected != _autoSyncInterval) {
|
||||||
@@ -226,41 +265,54 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
Future<void> _clearConfig() async {
|
Future<void> _clearConfig() async {
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) {
|
||||||
backgroundColor: Colors.white,
|
final colors = Theme.of(ctx).colorScheme;
|
||||||
elevation: 0,
|
return AlertDialog(
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
backgroundColor: colors.surface,
|
||||||
title: Row(
|
elevation: 0,
|
||||||
children: [
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
Container(
|
title: Row(
|
||||||
width: 40, height: 40,
|
children: [
|
||||||
decoration: BoxDecoration(color: Colors.red.withOpacity(0.08), borderRadius: BorderRadius.circular(10)),
|
Container(
|
||||||
child: const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.withOpacity(0.08), borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text('清除配置',
|
||||||
|
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||||
|
content: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16),
|
||||||
|
child: Text('确定要清除 WebDAV 配置吗?',
|
||||||
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6)),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
||||||
|
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||||
|
child: Text('取消',
|
||||||
|
style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6), fontSize: 14)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||||
|
child: const Text('清除',
|
||||||
|
style: TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.w600)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
|
||||||
const Text('清除配置', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
},
|
||||||
content: const Padding(
|
|
||||||
padding: EdgeInsets.only(top: 16),
|
|
||||||
child: Text('确定要清除 WebDAV 配置吗?', style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.6)),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.fromLTRB(24, 0, 24, 0),
|
|
||||||
actionsPadding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(ctx, false),
|
|
||||||
style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
|
||||||
child: const Text('取消', style: TextStyle(color: Color(0xFF666666), fontSize: 14)),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(ctx, true),
|
|
||||||
style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
|
||||||
child: const Text('清除', style: TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.w600)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
@@ -281,46 +333,54 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(title: const Text('WebDAV 备份')),
|
appBar: AppBar(title: const Text('WebDAV 备份')),
|
||||||
body: _isLoading && !_isConfigured
|
body: _isLoading && !_isConfigured
|
||||||
? const Center(child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A)))
|
? Center(child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary))
|
||||||
: ListView(
|
: ListView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// 已连接提示
|
// 已连接提示
|
||||||
if (_isConfigured) _buildConnectedBanner(),
|
if (_isConfigured) _buildConnectedBanner(colors),
|
||||||
|
|
||||||
// 服务器配置
|
// 服务器配置
|
||||||
_buildSectionLabel('服务器配置'),
|
_buildSectionLabel(colors, '服务器配置'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildInput(
|
_buildInput(
|
||||||
|
colors: colors,
|
||||||
controller: _urlController,
|
controller: _urlController,
|
||||||
hint: '服务器地址,如 https://dav.example.com',
|
hint: '服务器地址,如 https://dav.example.com',
|
||||||
icon: Icons.link,
|
icon: Icons.link,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildInput(
|
_buildInput(
|
||||||
|
colors: colors,
|
||||||
controller: _usernameController,
|
controller: _usernameController,
|
||||||
hint: '用户名',
|
hint: '用户名',
|
||||||
icon: Icons.person_outline,
|
icon: Icons.person_outline,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildInput(
|
_buildInput(
|
||||||
|
colors: colors,
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
hint: '密码',
|
hint: '密码',
|
||||||
icon: Icons.lock_outline,
|
icon: Icons.lock_outline,
|
||||||
obscure: _obscurePassword,
|
obscure: _obscurePassword,
|
||||||
suffix: GestureDetector(
|
suffix: GestureDetector(
|
||||||
onTap: () => setState(() => _obscurePassword = !_obscurePassword),
|
onTap: () => setState(() => _obscurePassword = !_obscurePassword),
|
||||||
child: Icon(_obscurePassword ? Icons.visibility_off : Icons.visibility, size: 20, color: const Color(0xFFBBBBBB)),
|
child: Icon(
|
||||||
|
_obscurePassword ? Icons.visibility_off : Icons.visibility,
|
||||||
|
size: 20,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.3)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildInput(
|
_buildInput(
|
||||||
|
colors: colors,
|
||||||
controller: _pathController,
|
controller: _pathController,
|
||||||
hint: '同步路径,如 /mooknote',
|
hint: '同步路径,如 /mooknote',
|
||||||
icon: Icons.folder_outlined,
|
icon: Icons.folder_outlined,
|
||||||
@@ -328,14 +388,15 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// 测试并保存
|
// 测试并保存
|
||||||
_buildBtn('测试并保存', onTap: _isLoading ? null : _saveConfig),
|
_buildBtn(colors, '测试并保存', onTap: _isLoading ? null : _saveConfig),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
if (_isConfigured) ...[
|
if (_isConfigured) ...[
|
||||||
// 自动同步
|
// 自动同步
|
||||||
_buildSectionLabel('自动同步'),
|
_buildSectionLabel(colors, '自动同步'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildSwitchRow(
|
_buildSwitchRow(
|
||||||
|
colors: colors,
|
||||||
icon: Icons.sync,
|
icon: Icons.sync,
|
||||||
label: '自动同步',
|
label: '自动同步',
|
||||||
sub: _isAutoSyncEnabled ? '每 $_autoSyncInterval 分钟自动同步' : '关闭',
|
sub: _isAutoSyncEnabled ? '每 $_autoSyncInterval 分钟自动同步' : '关闭',
|
||||||
@@ -345,6 +406,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
if (_isAutoSyncEnabled) ...[
|
if (_isAutoSyncEnabled) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
_buildTappableRow(
|
_buildTappableRow(
|
||||||
|
colors: colors,
|
||||||
label: '同步间隔',
|
label: '同步间隔',
|
||||||
value: '$_autoSyncInterval 分钟',
|
value: '$_autoSyncInterval 分钟',
|
||||||
onTap: _isLoading ? null : _showIntervalPicker,
|
onTap: _isLoading ? null : _showIntervalPicker,
|
||||||
@@ -354,27 +416,31 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
// 手动同步
|
// 手动同步
|
||||||
_buildSectionLabel('手动同步'),
|
_buildSectionLabel(colors, '手动同步'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _buildDirectionChip('上传到云端', SyncDirection.upload, Icons.upload)),
|
Expanded(
|
||||||
|
child: _buildDirectionChip(
|
||||||
|
colors, '上传到云端', SyncDirection.upload, Icons.upload)),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(child: _buildDirectionChip('下载到本地', SyncDirection.download, Icons.download)),
|
Expanded(
|
||||||
|
child: _buildDirectionChip(
|
||||||
|
colors, '下载到本地', SyncDirection.download, Icons.download)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildBtn('立即同步', onTap: _isLoading ? null : _syncData, loading: _isLoading),
|
_buildBtn(colors, '立即同步', onTap: _isLoading ? null : _syncData, loading: _isLoading),
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
// 清除配置
|
// 清除配置
|
||||||
_buildTextBtn('清除配置', onTap: _isLoading ? null : _clearConfig),
|
_buildTextBtn(colors, '清除配置', onTap: _isLoading ? null : _clearConfig),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
_buildTips(),
|
_buildTips(colors),
|
||||||
const SizedBox(height: 60),
|
const SizedBox(height: 60),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -383,40 +449,48 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
|
|
||||||
// ── widgets ─────────────────────────────────────────
|
// ── widgets ─────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildConnectedBanner() {
|
Widget _buildConnectedBanner(ColorScheme colors) {
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 24),
|
margin: const EdgeInsets.only(bottom: 24),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 6, height: 6,
|
width: 6,
|
||||||
|
height: 6,
|
||||||
decoration: const BoxDecoration(color: Color(0xFF4CAF50), shape: BoxShape.circle),
|
decoration: const BoxDecoration(color: Color(0xFF4CAF50), shape: BoxShape.circle),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_urlController.text,
|
_urlController.text,
|
||||||
style: const TextStyle(fontSize: 13, color: Color(0xFF666666)),
|
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6)),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Text('已连接', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
Text('已连接',
|
||||||
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSectionLabel(String text) {
|
Widget _buildSectionLabel(ColorScheme colors, String text) {
|
||||||
return Text(text, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF999999), letterSpacing: 0.5));
|
return Text(text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
|
letterSpacing: 0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInput({
|
Widget _buildInput({
|
||||||
|
required ColorScheme colors,
|
||||||
required TextEditingController controller,
|
required TextEditingController controller,
|
||||||
required String hint,
|
required String hint,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
@@ -426,35 +500,37 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
return TextField(
|
return TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
obscureText: obscure,
|
obscureText: obscure,
|
||||||
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
|
style: TextStyle(fontSize: 15, color: colors.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
hintStyle: const TextStyle(fontSize: 15, color: Color(0xFFBBBBBB)),
|
hintStyle: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||||
prefixIcon: Padding(
|
prefixIcon: Padding(
|
||||||
padding: const EdgeInsets.only(left: 4, right: 8),
|
padding: const EdgeInsets.only(left: 4, right: 8),
|
||||||
child: Icon(icon, size: 20, color: const Color(0xFFBBBBBB)),
|
child: Icon(icon, size: 20, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||||
),
|
),
|
||||||
prefixIconConstraints: const BoxConstraints(minWidth: 44),
|
prefixIconConstraints: const BoxConstraints(minWidth: 44),
|
||||||
suffixIcon: suffix != null ? Padding(
|
suffixIcon: suffix != null
|
||||||
padding: const EdgeInsets.only(right: 8),
|
? Padding(
|
||||||
child: suffix,
|
padding: const EdgeInsets.only(right: 8),
|
||||||
) : null,
|
child: suffix,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFFF8F8F8),
|
fillColor: colors.surfaceContainerHigh,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
borderSide: BorderSide.none,
|
borderSide: BorderSide.none,
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1),
|
borderSide: BorderSide(color: colors.primary, width: 1),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBtn(String text, {VoidCallback? onTap, bool loading = false}) {
|
Widget _buildBtn(ColorScheme colors, String text, {VoidCallback? onTap, bool loading = false}) {
|
||||||
final disabled = onTap == null;
|
final disabled = onTap == null;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@@ -462,30 +538,44 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: disabled ? const Color(0xFFDDDDDD) : const Color(0xFF1A1A1A),
|
color: disabled ? colors.onSurface.withValues(alpha: 0.15) : colors.primary,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: loading
|
child: loading
|
||||||
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white)))
|
? SizedBox(
|
||||||
: Text(text, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white)),
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2, valueColor: AlwaysStoppedAnimation(colors.onPrimary)))
|
||||||
|
: Text(text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15, fontWeight: FontWeight.w600, color: colors.onPrimary)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTextBtn(String text, {VoidCallback? onTap}) {
|
Widget _buildTextBtn(ColorScheme colors, String text, {VoidCallback? onTap}) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
child: Text(text, style: TextStyle(fontSize: 14, color: onTap == null ? const Color(0xFFCCCCCC) : const Color(0xFF999999)))),
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: onTap == null
|
||||||
|
? colors.onSurface.withValues(alpha: 0.25)
|
||||||
|
: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSwitchRow({
|
Widget _buildSwitchRow({
|
||||||
|
required ColorScheme colors,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String label,
|
required String label,
|
||||||
required String sub,
|
required String sub,
|
||||||
@@ -495,26 +585,30 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 20, color: value ? const Color(0xFF1A1A1A) : const Color(0xFFBBBBBB)),
|
Icon(icon,
|
||||||
|
size: 20,
|
||||||
|
color: value ? colors.primary : colors.onSurface.withValues(alpha: 0.3)),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
value ? sub : label,
|
value ? sub : label,
|
||||||
style: TextStyle(fontSize: 14, color: value ? const Color(0xFF666666) : const Color(0xFF1A1A1A)),
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: value ? colors.onSurface.withValues(alpha: 0.6) : colors.onSurface),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Switch(
|
Switch(
|
||||||
value: value,
|
value: value,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
activeColor: const Color(0xFF1A1A1A),
|
activeThumbColor: colors.primary,
|
||||||
activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3),
|
activeTrackColor: colors.primary.withValues(alpha: 0.3),
|
||||||
inactiveThumbColor: Colors.white,
|
inactiveThumbColor: colors.surface,
|
||||||
inactiveTrackColor: const Color(0xFFDDDDDD),
|
inactiveTrackColor: colors.onSurface.withValues(alpha: 0.15),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -522,6 +616,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTappableRow({
|
Widget _buildTappableRow({
|
||||||
|
required ColorScheme colors,
|
||||||
required String label,
|
required String label,
|
||||||
required String value,
|
required String value,
|
||||||
VoidCallback? onTap,
|
VoidCallback? onTap,
|
||||||
@@ -531,77 +626,99 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 32),
|
const SizedBox(width: 32),
|
||||||
Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF999999))),
|
Text(label,
|
||||||
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(value, style: const TextStyle(fontSize: 14, color: Color(0xFF666666))),
|
Text(value,
|
||||||
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
const Icon(Icons.chevron_right, size: 16, color: Color(0xFFCCCCCC)),
|
Icon(Icons.chevron_right,
|
||||||
|
size: 16, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDirectionChip(String label, SyncDirection dir, IconData icon) {
|
Widget _buildDirectionChip(ColorScheme colors, String label, SyncDirection dir, IconData icon) {
|
||||||
final selected = _syncDirection == dir;
|
final selected = _syncDirection == dir;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => setState(() => _syncDirection = dir),
|
onTap: () => setState(() => _syncDirection = dir),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFF8F8F8),
|
color: selected ? colors.primary : colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 16, color: selected ? Colors.white : const Color(0xFF999999)),
|
Icon(icon,
|
||||||
|
size: 16,
|
||||||
|
color: selected
|
||||||
|
? colors.onPrimary
|
||||||
|
: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: selected ? Colors.white : const Color(0xFF666666))),
|
Text(label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: selected
|
||||||
|
? colors.onPrimary
|
||||||
|
: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTips() {
|
Widget _buildTips(ColorScheme colors) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('支持的服务', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF999999), letterSpacing: 0.5)),
|
Text('支持的服务',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
|
letterSpacing: 0.5)),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_tip('坚果云、Nextcloud、AList 等 WebDAV 服务'),
|
_tip(colors, '坚果云、Nextcloud、AList 等 WebDAV 服务'),
|
||||||
_tip('服务器地址需包含 https://'),
|
_tip(colors, '服务器地址需包含 https://'),
|
||||||
_tip('首次同步可能需要较长时间'),
|
_tip(colors, '首次同步可能需要较长时间'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _tip(String text) {
|
Widget _tip(ColorScheme colors, String text) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 6),
|
padding: const EdgeInsets.only(bottom: 6),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: Icon(Icons.circle, size: 4, color: Color(0xFFCCCCCC)),
|
child: Icon(Icons.circle,
|
||||||
|
size: 4, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(child: Text(text, style: const TextStyle(fontSize: 13, color: Color(0xFF888888), height: 1.5))),
|
Expanded(
|
||||||
|
child: Text(text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5), height: 1.5))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
|
|
||||||
String get _currentType => _tabTypes[_currentIndex];
|
String get _currentType => _tabTypes[_currentIndex];
|
||||||
|
|
||||||
/// 计算标签使用次数
|
|
||||||
Map<String, int> _getTagUsageCounts(String type) {
|
Map<String, int> _getTagUsageCounts(String type) {
|
||||||
final provider = context.read<AppProvider>();
|
final provider = context.read<AppProvider>();
|
||||||
final counts = <String, int>{};
|
final counts = <String, int>{};
|
||||||
@@ -81,16 +80,17 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF8F8F8),
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('标签管理'),
|
title: const Text('标签管理'),
|
||||||
actions: [
|
actions: [
|
||||||
_isSyncing
|
_isSyncing
|
||||||
? const Padding(
|
? Padding(
|
||||||
padding: EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: SizedBox(width: 20, height: 20,
|
child: SizedBox(width: 20, height: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A))),
|
child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)),
|
||||||
)
|
)
|
||||||
: IconButton(
|
: IconButton(
|
||||||
icon: const Icon(Icons.sync, size: 20),
|
icon: const Icon(Icons.sync, size: 20),
|
||||||
@@ -112,7 +112,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 12, offset: const Offset(0, 4)),
|
BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 12, offset: const Offset(0, 4)),
|
||||||
@@ -121,10 +121,10 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.add, size: 18, color: Colors.white),
|
Icon(Icons.add, size: 18, color: colors.onPrimary),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text('添加${_typeLabels[_currentIndex]}',
|
Text('添加${_typeLabels[_currentIndex]}',
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -132,14 +132,13 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Tab 选择器 ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
Widget _buildTabSelector() {
|
Widget _buildTabSelector() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFEBEBEB),
|
color: colors.outlineVariant,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
),
|
),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
@@ -158,7 +157,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
padding: const EdgeInsets.all(3),
|
padding: const EdgeInsets.all(3),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(22),
|
borderRadius: BorderRadius.circular(22),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, 2)),
|
BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, 2)),
|
||||||
@@ -183,7 +182,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(_typeLabels[i],
|
child: Text(_typeLabels[i],
|
||||||
style: TextStyle(fontSize: 13, fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
style: TextStyle(fontSize: 13, fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
||||||
color: selected ? const Color(0xFF1A1A1A) : const Color(0xFF999999))),
|
color: selected ? colors.onSurface : colors.onSurface.withValues(alpha: 0.4))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -197,9 +196,8 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 标签列表 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
Widget _buildTagList(String type) {
|
Widget _buildTagList(String type) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final tags = _tagCache[type] ?? [];
|
final tags = _tagCache[type] ?? [];
|
||||||
final usageCounts = _getTagUsageCounts(type);
|
final usageCounts = _getTagUsageCounts(type);
|
||||||
|
|
||||||
@@ -207,7 +205,6 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
return _buildEmptyState(type);
|
return _buildEmptyState(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按使用次数降序排序
|
|
||||||
final sorted = List<Map<String, dynamic>>.from(tags)
|
final sorted = List<Map<String, dynamic>>.from(tags)
|
||||||
..sort((a, b) {
|
..sort((a, b) {
|
||||||
final ca = usageCounts[a['name']] ?? 0;
|
final ca = usageCounts[a['name']] ?? 0;
|
||||||
@@ -220,18 +217,16 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 统计行
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text('共 ${tags.length} 个标签', style: const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))),
|
Text('共 ${tags.length} 个标签', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text('已使用 ${usageCounts.length} 个', style: const TextStyle(fontSize: 12, color: Color(0xFFD0D0D0))),
|
Text('已使用 ${usageCounts.length} 个', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.2))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 标签列表
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.only(bottom: 80),
|
padding: const EdgeInsets.only(bottom: 80),
|
||||||
@@ -248,50 +243,50 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTagChip(Map<String, dynamic> tag, Map<String, int> usageCounts) {
|
Widget _buildTagChip(Map<String, dynamic> tag, Map<String, int> usageCounts) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final name = tag['name'] as String;
|
final name = tag['name'] as String;
|
||||||
final count = usageCounts[name] ?? 0;
|
final count = usageCounts[name] ?? 0;
|
||||||
|
|
||||||
return Container(
|
return GestureDetector(
|
||||||
padding: const EdgeInsets.only(left: 14, right: 6, top: 8, bottom: 8),
|
onLongPress: () => _showRenameDialog(tag),
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
color: Colors.white,
|
padding: const EdgeInsets.only(left: 14, right: 6, top: 8, bottom: 8),
|
||||||
borderRadius: BorderRadius.circular(20),
|
decoration: BoxDecoration(
|
||||||
boxShadow: [
|
color: colors.surface,
|
||||||
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: const Offset(0, 2)),
|
borderRadius: BorderRadius.circular(20),
|
||||||
],
|
boxShadow: [
|
||||||
),
|
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: const Offset(0, 2)),
|
||||||
child: Row(
|
],
|
||||||
mainAxisSize: MainAxisSize.min,
|
),
|
||||||
children: [
|
child: Row(
|
||||||
// 标签名区域 — 点击改名
|
mainAxisSize: MainAxisSize.min,
|
||||||
GestureDetector(
|
children: [
|
||||||
onTap: () => _showRenameDialog(tag),
|
Text(name, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
child: Text(name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
if (count > 0) ...[
|
||||||
),
|
const SizedBox(width: 6),
|
||||||
if (count > 0) ...[
|
Container(
|
||||||
const SizedBox(width: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
Container(
|
decoration: BoxDecoration(color: colors.outlineVariant, borderRadius: BorderRadius.circular(8)),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
child: Text('$count', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
decoration: BoxDecoration(color: const Color(0xFFF0F0F0), borderRadius: BorderRadius.circular(8)),
|
),
|
||||||
child: Text('$count', style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: Color(0xFF999999))),
|
],
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _showDeleteDialog(tag),
|
||||||
|
child: Container(
|
||||||
|
width: 22, height: 22,
|
||||||
|
decoration: BoxDecoration(color: colors.outlineVariant, borderRadius: BorderRadius.circular(11)),
|
||||||
|
child: Icon(Icons.close, size: 12, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(width: 4),
|
),
|
||||||
// X 按钮 — 点击删除
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => _showDeleteDialog(tag),
|
|
||||||
child: Container(
|
|
||||||
width: 22, height: 22,
|
|
||||||
decoration: BoxDecoration(color: const Color(0xFFF0F0F0), borderRadius: BorderRadius.circular(11)),
|
|
||||||
child: const Icon(Icons.close, size: 12, color: Color(0xFF999999)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState(String type) {
|
Widget _buildEmptyState(String type) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final idx = _tabTypes.indexOf(type);
|
final idx = _tabTypes.indexOf(type);
|
||||||
final icon = _typeIcons[idx];
|
final icon = _typeIcons[idx];
|
||||||
final label = _typeLabels[idx];
|
final label = _typeLabels[idx];
|
||||||
@@ -303,57 +298,56 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 64, height: 64,
|
width: 64, height: 64,
|
||||||
decoration: BoxDecoration(color: const Color(0xFFECECEC), borderRadius: BorderRadius.circular(18)),
|
decoration: BoxDecoration(color: colors.outlineVariant, borderRadius: BorderRadius.circular(18)),
|
||||||
child: Icon(icon, size: 28, color: const Color(0xFFCCCCCC)),
|
child: Icon(icon, size: 28, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text('暂无$label',
|
Text('暂无$label',
|
||||||
style: const TextStyle(fontSize: 14, color: Color(0xFFBBBBBB), fontWeight: FontWeight.w500)),
|
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.3), fontWeight: FontWeight.w500)),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(hints[idx], style: const TextStyle(fontSize: 12, color: Color(0xFFD5D5D5))),
|
Text(hints[idx], style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.15))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 添加标签 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
void _showAddDialog() {
|
void _showAddDialog() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final controller = TextEditingController();
|
final controller = TextEditingController();
|
||||||
final type = _currentType;
|
final type = _currentType;
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
title: Text('添加${_typeLabels[_currentIndex]}',
|
title: Text('添加${_typeLabels[_currentIndex]}',
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller, autofocus: true,
|
controller: controller, autofocus: true,
|
||||||
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
|
style: TextStyle(fontSize: 15, color: colors.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '输入标签名称',
|
hintText: '输入标签名称',
|
||||||
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)),
|
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||||
filled: true, fillColor: const Color(0xFFFAFAFA),
|
filled: true, fillColor: colors.surfaceContainerHigh,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
||||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1)),
|
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: colors.primary, width: 1)),
|
||||||
),
|
),
|
||||||
onSubmitted: (value) => _doAddTag(ctx, controller.text.trim(), type),
|
onSubmitted: (value) => _doAddTag(ctx, controller.text.trim(), type),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))),
|
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(20)),
|
decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => _doAddTag(ctx, controller.text.trim(), type),
|
onTap: () => _doAddTag(ctx, controller.text.trim(), type),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: const Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||||
child: Text('添加', style: TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500)),
|
child: Text('添加', style: TextStyle(fontSize: 14, color: colors.onPrimary, fontWeight: FontWeight.w500)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -378,9 +372,8 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
await _loadTags(type);
|
await _loadTags(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 重命名 ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
void _showRenameDialog(Map<String, dynamic> tag) {
|
void _showRenameDialog(Map<String, dynamic> tag) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final controller = TextEditingController(text: tag['name'] as String);
|
final controller = TextEditingController(text: tag['name'] as String);
|
||||||
final tagId = tag['id'] as String;
|
final tagId = tag['id'] as String;
|
||||||
final type = tag['type'] as String;
|
final type = tag['type'] as String;
|
||||||
@@ -389,34 +382,34 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
title: const Text('重命名标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
title: Text('重命名标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller, autofocus: true,
|
controller: controller, autofocus: true,
|
||||||
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
|
style: TextStyle(fontSize: 15, color: colors.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '输入新名称',
|
hintText: '输入新名称',
|
||||||
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)),
|
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||||
filled: true, fillColor: const Color(0xFFFAFAFA),
|
filled: true, fillColor: colors.surfaceContainerHigh,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
||||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1)),
|
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: colors.primary, width: 1)),
|
||||||
),
|
),
|
||||||
onSubmitted: (value) => _doRenameTag(ctx, tagId, value.trim(), type, oldName),
|
onSubmitted: (value) => _doRenameTag(ctx, tagId, value.trim(), type, oldName),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))),
|
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(20)),
|
decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => _doRenameTag(ctx, tagId, controller.text.trim(), type, oldName),
|
onTap: () => _doRenameTag(ctx, tagId, controller.text.trim(), type, oldName),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: const Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||||
child: Text('确定', style: TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500)),
|
child: Text('确定', style: TextStyle(fontSize: 14, color: colors.onPrimary, fontWeight: FontWeight.w500)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -439,9 +432,8 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
if (success) await _loadTags(type);
|
if (success) await _loadTags(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 删除(长按触发)──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
void _showDeleteDialog(Map<String, dynamic> tag) {
|
void _showDeleteDialog(Map<String, dynamic> tag) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final tagId = tag['id'] as String;
|
final tagId = tag['id'] as String;
|
||||||
final type = tag['type'] as String;
|
final type = tag['type'] as String;
|
||||||
final name = tag['name'] as String;
|
final name = tag['name'] as String;
|
||||||
@@ -457,17 +449,17 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => StatefulBuilder(
|
builder: (ctx) => StatefulBuilder(
|
||||||
builder: (ctx, setDialogState) => AlertDialog(
|
builder: (ctx, setDialogState) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(12)),
|
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(12)),
|
||||||
child: Text(name, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))),
|
child: Text(name, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
const Text('删除标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
Text('删除标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||||
@@ -481,7 +473,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
const Text('删除后对已有条目的影响:', style: TextStyle(fontSize: 13, color: Color(0xFF999999))),
|
Text('删除后对已有条目的影响:', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildDeleteOption(value: 'remove', groupValue: selectedAction, onChanged: (v) => setDialogState(() => selectedAction = v), title: '从所有条目中移除该标签', subtitle: '标签将从影视/书籍/笔记中清除'),
|
_buildDeleteOption(value: 'remove', groupValue: selectedAction, onChanged: (v) => setDialogState(() => selectedAction = v), title: '从所有条目中移除该标签', subtitle: '标签将从影视/书籍/笔记中清除'),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
@@ -504,19 +496,19 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5),
|
color: isSelected ? colors.primary : colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: isSelected ? colors.primary : colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Text(t, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: isSelected ? Colors.white : const Color(0xFF555555))),
|
child: Text(t, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.7))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
)
|
)
|
||||||
: Container(
|
: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||||
decoration: BoxDecoration(color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(12)),
|
decoration: BoxDecoration(color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(12)),
|
||||||
child: const Text('无其他标签可替换', style: TextStyle(fontSize: 13, color: Color(0xFFAAAAAA))),
|
child: Text('无其他标签可替换', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -526,7 +518,7 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))),
|
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(color: const Color(0xFFE53935), borderRadius: BorderRadius.circular(20)),
|
decoration: BoxDecoration(color: const Color(0xFFE53935), borderRadius: BorderRadius.circular(20)),
|
||||||
child: Material(
|
child: Material(
|
||||||
@@ -573,15 +565,16 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
required String title,
|
required String title,
|
||||||
String? subtitle,
|
String? subtitle,
|
||||||
}) {
|
}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final selected = value == groupValue;
|
final selected = value == groupValue;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => onChanged(value),
|
onTap: () => onChanged(value),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: selected ? const Color(0xFFFAFAFA) : Colors.white,
|
color: selected ? colors.surfaceContainerHigh : colors.surface,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFEEEEEE), width: selected ? 1 : 0.5),
|
border: Border.all(color: selected ? colors.primary : colors.outlineVariant, width: selected ? 1 : 0.5),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -589,14 +582,14 @@ class _TagManagementPageState extends State<TagManagementPage> {
|
|||||||
width: 18, height: 18,
|
width: 18, height: 18,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFCCCCCC), width: selected ? 5 : 1.5),
|
border: Border.all(color: selected ? colors.primary : colors.onSurface.withValues(alpha: 0.25), width: selected ? 5 : 1.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
||||||
Text(title, style: TextStyle(fontSize: 14, fontWeight: selected ? FontWeight.w500 : FontWeight.normal, color: selected ? const Color(0xFF1A1A1A) : const Color(0xFF666666))),
|
Text(title, style: TextStyle(fontSize: 14, fontWeight: selected ? FontWeight.w500 : FontWeight.normal, color: selected ? colors.onSurface : colors.onSurface.withValues(alpha: 0.6))),
|
||||||
if (subtitle != null) Padding(padding: const EdgeInsets.only(top: 2), child: Text(subtitle, style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)))),
|
if (subtitle != null) Padding(padding: const EdgeInsets.only(top: 2), child: Text(subtitle, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35)))),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -32,13 +32,16 @@ class AppProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
// 当前主界面选中的标签 (0: 观影,1: 阅读,2: 笔记)
|
// 当前主界面选中的标签 (0: 观影,1: 阅读,2: 笔记)
|
||||||
int _mainTabIndex = 0;
|
int _mainTabIndex = 0;
|
||||||
|
|
||||||
// 当前底部导航选中的索引 (0: 主页,1: 新增,2: 我的)
|
// 当前底部导航选中的索引 (0: 主页,1: 新增,2: 我的)
|
||||||
int _bottomNavIndex = 0;
|
int _bottomNavIndex = 0;
|
||||||
|
|
||||||
// 底部导航栏是否可见
|
// 底部导航栏是否可见
|
||||||
bool _bottomNavVisible = true;
|
bool _bottomNavVisible = true;
|
||||||
|
|
||||||
|
// 主题模式
|
||||||
|
ThemeMode _themeMode = ThemeMode.system;
|
||||||
|
|
||||||
/// 是否使用远程服务端(同步开关 + 已激活)
|
/// 是否使用远程服务端(同步开关 + 已激活)
|
||||||
bool get _useRemote {
|
bool get _useRemote {
|
||||||
final prefs = UserPrefs();
|
final prefs = UserPrefs();
|
||||||
@@ -134,6 +137,7 @@ class AppProvider extends ChangeNotifier {
|
|||||||
int get bookStatusIndex => _bookStatusIndex;
|
int get bookStatusIndex => _bookStatusIndex;
|
||||||
bool get drawerOpen => _drawerOpen;
|
bool get drawerOpen => _drawerOpen;
|
||||||
bool get bottomNavVisible => _bottomNavVisible;
|
bool get bottomNavVisible => _bottomNavVisible;
|
||||||
|
ThemeMode get themeMode => _themeMode;
|
||||||
List<Movie> get movies => _movies;
|
List<Movie> get movies => _movies;
|
||||||
List<Book> get books => _books;
|
List<Book> get books => _books;
|
||||||
List<Note> get notes => _notes;
|
List<Note> get notes => _notes;
|
||||||
@@ -167,6 +171,26 @@ class AppProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setThemeMode(ThemeMode mode) {
|
||||||
|
if (_themeMode != mode) {
|
||||||
|
_themeMode = mode;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadThemeMode() {
|
||||||
|
final prefs = UserPrefs();
|
||||||
|
switch (prefs.themeMode) {
|
||||||
|
case 1:
|
||||||
|
_themeMode = ThemeMode.light;
|
||||||
|
case 2:
|
||||||
|
_themeMode = ThemeMode.dark;
|
||||||
|
default:
|
||||||
|
_themeMode = ThemeMode.system;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void setMovieStatusIndex(int index) {
|
void setMovieStatusIndex(int index) {
|
||||||
_movieStatusIndex = index;
|
_movieStatusIndex = index;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ class AppTheme {
|
|||||||
onSurface: _black,
|
onSurface: _black,
|
||||||
error: error,
|
error: error,
|
||||||
onError: _white,
|
onError: _white,
|
||||||
|
surfaceContainerHighest: _offWhite,
|
||||||
|
surfaceContainerHigh: Color(0xFFFAFAFA),
|
||||||
|
outline: _lighterGray,
|
||||||
|
outlineVariant: Color(0xFFF0F0F0),
|
||||||
),
|
),
|
||||||
|
|
||||||
// AppBar - 极简无边框
|
// AppBar - 极简无边框
|
||||||
@@ -262,6 +266,10 @@ class AppTheme {
|
|||||||
onSurface: _white,
|
onSurface: _white,
|
||||||
error: Color(0xFFEF4444),
|
error: Color(0xFFEF4444),
|
||||||
onError: _black,
|
onError: _black,
|
||||||
|
surfaceContainerHighest: _darkGray,
|
||||||
|
surfaceContainerHigh: Color(0xFF2A2A2A),
|
||||||
|
outline: _gray,
|
||||||
|
outlineVariant: _darkGray,
|
||||||
),
|
),
|
||||||
|
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
@@ -286,6 +294,12 @@ class AppTheme {
|
|||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
listTileTheme: const ListTileThemeData(
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
minLeadingWidth: 0,
|
||||||
|
dense: true,
|
||||||
|
),
|
||||||
|
|
||||||
dividerTheme: DividerThemeData(
|
dividerTheme: DividerThemeData(
|
||||||
color: _darkGray,
|
color: _darkGray,
|
||||||
@@ -304,6 +318,9 @@ class AppTheme {
|
|||||||
focusedBorder: UnderlineInputBorder(
|
focusedBorder: UnderlineInputBorder(
|
||||||
borderSide: BorderSide(color: _white, width: 1),
|
borderSide: BorderSide(color: _white, width: 1),
|
||||||
),
|
),
|
||||||
|
errorBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Color(0xFFEF4444), width: 0.5),
|
||||||
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 12),
|
contentPadding: EdgeInsets.symmetric(vertical: 12),
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontFamily: _fontFamily,
|
fontFamily: _fontFamily,
|
||||||
@@ -311,6 +328,12 @@ class AppTheme {
|
|||||||
fontWeight: _regular,
|
fontWeight: _regular,
|
||||||
color: _gray,
|
color: _gray,
|
||||||
),
|
),
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: _medium,
|
||||||
|
color: _lightGray,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
@@ -327,6 +350,11 @@ class AppTheme {
|
|||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: _white,
|
foregroundColor: _white,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
textStyle: TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: _medium,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -335,6 +363,84 @@ class AppTheme {
|
|||||||
selectedItemColor: _white,
|
selectedItemColor: _white,
|
||||||
unselectedItemColor: _gray,
|
unselectedItemColor: _gray,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
type: BottomNavigationBarType.fixed,
|
||||||
|
selectedLabelStyle: TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: _medium,
|
||||||
|
),
|
||||||
|
unselectedLabelStyle: TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: _regular,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
textTheme: const TextTheme(
|
||||||
|
headlineLarge: TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: _semibold,
|
||||||
|
color: _white,
|
||||||
|
letterSpacing: -0.5,
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
headlineMedium: TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: _semibold,
|
||||||
|
color: _white,
|
||||||
|
letterSpacing: -0.3,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
headlineSmall: TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: _semibold,
|
||||||
|
color: _white,
|
||||||
|
letterSpacing: -0.2,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
bodyLarge: TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: _regular,
|
||||||
|
color: _offWhite,
|
||||||
|
height: 1.6,
|
||||||
|
),
|
||||||
|
bodyMedium: TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: _regular,
|
||||||
|
color: _offWhite,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
bodySmall: TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: _regular,
|
||||||
|
color: _lightGray,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
labelLarge: TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: _medium,
|
||||||
|
color: _white,
|
||||||
|
),
|
||||||
|
labelMedium: TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: _medium,
|
||||||
|
color: _lightGray,
|
||||||
|
),
|
||||||
|
labelSmall: TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: _medium,
|
||||||
|
color: _gray,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ class UsageStatsService with WidgetsBindingObserver {
|
|||||||
final UserPrefs _prefs = UserPrefs();
|
final UserPrefs _prefs = UserPrefs();
|
||||||
|
|
||||||
/// 统计服务器地址,发布前替换为实际地址,置空则禁用
|
/// 统计服务器地址,发布前替换为实际地址,置空则禁用
|
||||||
static String serverUrl = 'http://api.mooknote.iletter.top/';
|
// static String serverUrl = 'http://api.mooknote.iletter.top/';
|
||||||
|
static String serverUrl = 'http://192.168.31.48:27050/';
|
||||||
Timer? _heartbeatTimer;
|
Timer? _heartbeatTimer;
|
||||||
bool _started = false;
|
bool _started = false;
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ class UserPrefs {
|
|||||||
/// 初始化
|
/// 初始化
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
_prefs = await SharedPreferences.getInstance();
|
_prefs = await SharedPreferences.getInstance();
|
||||||
|
// 迁移旧版 isDarkMode 布尔值到新版 themeMode 三态值
|
||||||
|
if (_prefs!.containsKey('isDarkMode') && !_prefs!.containsKey('themeMode')) {
|
||||||
|
final oldValue = _prefs!.getBool('isDarkMode') ?? false;
|
||||||
|
await _prefs!.setInt('themeMode', oldValue ? 2 : 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取实例
|
/// 获取实例
|
||||||
@@ -38,9 +43,9 @@ class UserPrefs {
|
|||||||
|
|
||||||
// ========== 应用设置 ==========
|
// ========== 应用设置 ==========
|
||||||
|
|
||||||
/// 是否暗黑模式
|
/// 主题模式: 0=跟随系统, 1=浅色, 2=深色
|
||||||
bool get isDarkMode => prefs.getBool('isDarkMode') ?? false;
|
int get themeMode => prefs.getInt('themeMode') ?? 0;
|
||||||
Future<bool> setDarkMode(bool value) => prefs.setBool('isDarkMode', value);
|
Future<bool> setThemeMode(int value) => prefs.setInt('themeMode', value);
|
||||||
|
|
||||||
/// 是否首次启动
|
/// 是否首次启动
|
||||||
bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true;
|
bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class _AnimatedStarRatingState extends State<AnimatedStarRating>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final starValue = widget.rating / 2;
|
final starValue = widget.rating / 2;
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -84,7 +85,7 @@ class _AnimatedStarRatingState extends State<AnimatedStarRating>
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: widget.starSize,
|
fontSize: widget.starSize,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: const Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ class AppRefreshIndicator extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: onRefresh,
|
onRefresh: onRefresh,
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
strokeWidth: 2.5,
|
strokeWidth: 2.5,
|
||||||
displacement: 60,
|
displacement: 60,
|
||||||
semanticsLabel: semanticsLabel,
|
semanticsLabel: semanticsLabel,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class BookListItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pushNamed(context, '/book-detail', arguments: book);
|
Navigator.pushNamed(context, '/book-detail', arguments: book);
|
||||||
@@ -23,28 +24,21 @@ class BookListItem extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 封面
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildCover(),
|
child: _buildCover(colors),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// 书名
|
|
||||||
Text(
|
Text(
|
||||||
book.title,
|
book.title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
// 评分
|
|
||||||
if (book.rating != null)
|
if (book.rating != null)
|
||||||
AnimatedStarRating(rating: book.rating!, starSize: 12, showNumber: true)
|
AnimatedStarRating(rating: book.rating!, starSize: 12, showNumber: true)
|
||||||
else
|
else
|
||||||
@@ -53,45 +47,45 @@ class BookListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建封面
|
Widget _buildCover(ColorScheme colors) {
|
||||||
Widget _buildCover() {
|
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5),
|
border: Border.all(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: FadeInLocalImage(
|
child: FadeInLocalImage(
|
||||||
path: book.coverPath,
|
path: book.coverPath,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: const Center(child: Icon(Icons.menu_book_outlined, size: 32, color: Color(0xFFCCCCCC))),
|
placeholder: Center(child: Icon(Icons.menu_book_outlined, size: 32, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||||
errorWidget: const Center(child: Icon(Icons.menu_book_outlined, size: 32, color: Color(0xFFCCCCCC))),
|
errorWidget: Center(child: Icon(Icons.menu_book_outlined, size: 32, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示删除确认对话框
|
|
||||||
void _showDeleteDialog(BuildContext context) {
|
void _showDeleteDialog(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'确认删除',
|
'确认删除',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
content: Text(
|
content: Text(
|
||||||
'确定要删除《${book.title}》吗?删除后可在回收站恢复。',
|
'确定要删除《${book.title}》吗?删除后可在回收站恢复。',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -99,7 +93,7 @@ class BookListItem extends StatelessWidget {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFF666666),
|
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
child: const Text('取消'),
|
child: const Text('取消'),
|
||||||
@@ -111,8 +105,8 @@ class BookListItem extends StatelessWidget {
|
|||||||
ToastUtil.show(context, '已删除');
|
ToastUtil.show(context, '已删除');
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: colors.error,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colors.onError,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
|||||||
@@ -8,20 +8,21 @@ class BookStatusBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
|
bottom: BorderSide(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
),
|
),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
@@ -42,7 +43,7 @@ class BookStatusBar extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(3),
|
padding: const EdgeInsets.all(3),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -58,18 +59,21 @@ class BookStatusBar extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_buildTab(
|
_buildTab(
|
||||||
|
colors: colors,
|
||||||
label: '已读',
|
label: '已读',
|
||||||
icon: Icons.check_circle_outline,
|
icon: Icons.check_circle_outline,
|
||||||
isSelected: provider.bookStatusIndex == 0,
|
isSelected: provider.bookStatusIndex == 0,
|
||||||
onTap: () => provider.setBookStatusIndex(0),
|
onTap: () => provider.setBookStatusIndex(0),
|
||||||
),
|
),
|
||||||
_buildTab(
|
_buildTab(
|
||||||
|
colors: colors,
|
||||||
label: '在读',
|
label: '在读',
|
||||||
icon: Icons.menu_book_outlined,
|
icon: Icons.menu_book_outlined,
|
||||||
isSelected: provider.bookStatusIndex == 1,
|
isSelected: provider.bookStatusIndex == 1,
|
||||||
onTap: () => provider.setBookStatusIndex(1),
|
onTap: () => provider.setBookStatusIndex(1),
|
||||||
),
|
),
|
||||||
_buildTab(
|
_buildTab(
|
||||||
|
colors: colors,
|
||||||
label: '想读',
|
label: '想读',
|
||||||
icon: Icons.bookmark_outlined,
|
icon: Icons.bookmark_outlined,
|
||||||
isSelected: provider.bookStatusIndex == 2,
|
isSelected: provider.bookStatusIndex == 2,
|
||||||
@@ -89,6 +93,7 @@ class BookStatusBar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTab({
|
Widget _buildTab({
|
||||||
|
required ColorScheme colors,
|
||||||
required String label,
|
required String label,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required bool isSelected,
|
required bool isSelected,
|
||||||
@@ -108,7 +113,7 @@ class BookStatusBar extends StatelessWidget {
|
|||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: isSelected ? Colors.white : const Color(0xFF666666),
|
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
@@ -116,7 +121,7 @@ class BookStatusBar extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||||
color: isSelected ? Colors.white : const Color(0xFF666666),
|
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,34 +8,33 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// 获取底部安全区域高度
|
|
||||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
return Container(
|
return Container(
|
||||||
// 高度:导航栏本身高度 + 底部安全距离 + 上下边距
|
|
||||||
height: 64 + bottomPadding + 16,
|
height: 64 + bottomPadding + 16,
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Dock栏主体 - 悬浮效果
|
|
||||||
Container(
|
Container(
|
||||||
height: 56,
|
height: 56,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 40),
|
margin: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(28),
|
borderRadius: BorderRadius.circular(28),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.08),
|
color: Colors.black.withOpacity(isDark ? 0.3 : 0.08),
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
spreadRadius: 0,
|
spreadRadius: 0,
|
||||||
),
|
),
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.04),
|
color: Colors.black.withOpacity(isDark ? 0.15 : 0.04),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
spreadRadius: -2,
|
spreadRadius: -2,
|
||||||
@@ -45,19 +44,16 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
// 主页按钮
|
|
||||||
_buildNavItem(
|
_buildNavItem(
|
||||||
|
colors: colors,
|
||||||
icon: Icons.home_outlined,
|
icon: Icons.home_outlined,
|
||||||
activeIcon: Icons.home,
|
activeIcon: Icons.home,
|
||||||
isActive: provider.bottomNavIndex == 0,
|
isActive: provider.bottomNavIndex == 0,
|
||||||
onTap: () => provider.setBottomNavIndex(0),
|
onTap: () => provider.setBottomNavIndex(0),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 中间新增按钮
|
|
||||||
_buildAddButton(context, provider),
|
_buildAddButton(context, provider),
|
||||||
|
|
||||||
// 我的按钮
|
|
||||||
_buildNavItem(
|
_buildNavItem(
|
||||||
|
colors: colors,
|
||||||
icon: Icons.person_outline,
|
icon: Icons.person_outline,
|
||||||
activeIcon: Icons.person,
|
activeIcon: Icons.person,
|
||||||
isActive: provider.bottomNavIndex == 2,
|
isActive: provider.bottomNavIndex == 2,
|
||||||
@@ -66,7 +62,6 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 底部安全距离占位
|
|
||||||
SizedBox(height: bottomPadding + 8),
|
SizedBox(height: bottomPadding + 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -74,9 +69,9 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建导航项
|
|
||||||
Widget _buildNavItem({
|
Widget _buildNavItem({
|
||||||
|
required ColorScheme colors,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required IconData activeIcon,
|
required IconData activeIcon,
|
||||||
required bool isActive,
|
required bool isActive,
|
||||||
@@ -92,16 +87,16 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
isActive ? activeIcon : icon,
|
isActive ? activeIcon : icon,
|
||||||
color: isActive ? const Color(0xFF1A1A1A) : const Color(0xFF999999),
|
color: isActive ? colors.primary : colors.onSurface.withValues(alpha: 0.4),
|
||||||
size: 26,
|
size: 26,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建中间新增按钮
|
|
||||||
Widget _buildAddButton(BuildContext context, AppProvider provider) {
|
Widget _buildAddButton(BuildContext context, AppProvider provider) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => _showAddDialog(context, provider),
|
onTap: () => _showAddDialog(context, provider),
|
||||||
onLongPress: () => _showQuickAddDialog(context, provider),
|
onLongPress: () => _showQuickAddDialog(context, provider),
|
||||||
@@ -109,25 +104,23 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
width: 44,
|
width: 44,
|
||||||
height: 44,
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.add,
|
Icons.add,
|
||||||
color: Colors.white,
|
color: colors.onPrimary,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 长按快速添加 - 根据当前界面直接跳转到对应添加界面
|
|
||||||
void _showQuickAddDialog(BuildContext context, AppProvider provider) {
|
void _showQuickAddDialog(BuildContext context, AppProvider provider) {
|
||||||
// 根据当前主标签页决定跳转到哪个添加界面
|
|
||||||
final currentTab = provider.mainTabIndex;
|
final currentTab = provider.mainTabIndex;
|
||||||
|
|
||||||
switch (currentTab) {
|
switch (currentTab) {
|
||||||
case 0: // 观影标签页
|
case 0:
|
||||||
final statusMap = {
|
final statusMap = {
|
||||||
0: 'watched',
|
0: 'watched',
|
||||||
1: 'watching',
|
1: 'watching',
|
||||||
@@ -140,7 +133,7 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
arguments: {'initialStatus': currentStatus},
|
arguments: {'initialStatus': currentStatus},
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 1: // 阅读标签页
|
case 1:
|
||||||
final statusMap = {
|
final statusMap = {
|
||||||
0: 'read',
|
0: 'read',
|
||||||
1: 'reading',
|
1: 'reading',
|
||||||
@@ -153,7 +146,7 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
arguments: {'initialStatus': currentStatus},
|
arguments: {'initialStatus': currentStatus},
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 2: // 笔记标签页
|
case 2:
|
||||||
Navigator.pushNamed(context, '/note-form');
|
Navigator.pushNamed(context, '/note-form');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -161,34 +154,33 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示新增对话框
|
|
||||||
void _showAddDialog(BuildContext context, AppProvider provider) {
|
void _showAddDialog(BuildContext context, AppProvider provider) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
),
|
),
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
|
final bottomColors = Theme.of(context).colorScheme;
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// 顶部指示条
|
|
||||||
Container(
|
Container(
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 4,
|
height: 4,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFE0E0E0),
|
color: bottomColors.onSurface.withValues(alpha: 0.15),
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
// 标题
|
Padding(
|
||||||
const Padding(
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
@@ -196,15 +188,15 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: bottomColors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 选项列表
|
|
||||||
_buildAddOption(
|
_buildAddOption(
|
||||||
|
colors: bottomColors,
|
||||||
icon: Icons.movie_outlined,
|
icon: Icons.movie_outlined,
|
||||||
title: '添加观影',
|
title: '添加观影',
|
||||||
subtitle: '记录你看过的电影',
|
subtitle: '记录你看过的电影',
|
||||||
@@ -224,6 +216,7 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildAddOption(
|
_buildAddOption(
|
||||||
|
colors: bottomColors,
|
||||||
icon: Icons.menu_book_outlined,
|
icon: Icons.menu_book_outlined,
|
||||||
title: '添加阅读',
|
title: '添加阅读',
|
||||||
subtitle: '记录你读过的书',
|
subtitle: '记录你读过的书',
|
||||||
@@ -243,6 +236,7 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildAddOption(
|
_buildAddOption(
|
||||||
|
colors: bottomColors,
|
||||||
icon: Icons.note_outlined,
|
icon: Icons.note_outlined,
|
||||||
title: '添加笔记',
|
title: '添加笔记',
|
||||||
subtitle: '记录你的想法和笔记',
|
subtitle: '记录你的想法和笔记',
|
||||||
@@ -259,8 +253,8 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建新增选项
|
|
||||||
Widget _buildAddOption({
|
Widget _buildAddOption({
|
||||||
|
required ColorScheme colors,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String title,
|
required String title,
|
||||||
required String subtitle,
|
required String subtitle,
|
||||||
@@ -276,13 +270,13 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
width: 44,
|
width: 44,
|
||||||
height: 44,
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 22,
|
size: 22,
|
||||||
color: const Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
@@ -292,26 +286,26 @@ class CustomBottomNavBar extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
subtitle,
|
subtitle,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.chevron_right,
|
Icons.chevron_right,
|
||||||
color: Color(0xFFCCCCCC),
|
color: colors.onSurface.withValues(alpha: 0.25),
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -33,36 +33,25 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Drawer(
|
return Drawer(
|
||||||
backgroundColor: const Color(0xFFF8F8F8),
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 头像 + 统计(独立卡片区域)
|
|
||||||
_buildProfileCard(context),
|
_buildProfileCard(context),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 热力图
|
|
||||||
_buildCalendarSection(context),
|
_buildCalendarSection(context),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 最近添加
|
|
||||||
_buildRecentSection(context),
|
_buildRecentSection(context),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 功能入口
|
|
||||||
_buildToolsCard(context),
|
_buildToolsCard(context),
|
||||||
|
|
||||||
// 底部版本号
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 32),
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 32),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text('v$_version', style: const TextStyle(fontSize: 11, color: Color(0xFFD0D0D0))),
|
child: Text('v$_version', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.2))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -77,6 +66,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
Widget _buildProfileCard(BuildContext context) {
|
Widget _buildProfileCard(BuildContext context) {
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final userPrefs = UserPrefs();
|
final userPrefs = UserPrefs();
|
||||||
final nickname = userPrefs.nickname;
|
final nickname = userPrefs.nickname;
|
||||||
final motto = userPrefs.motto;
|
final motto = userPrefs.motto;
|
||||||
@@ -89,77 +79,69 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 头像 + 名称/座右铭 + 统计
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
// 头像
|
|
||||||
Container(
|
Container(
|
||||||
width: 52,
|
width: 52,
|
||||||
height: 52,
|
height: 52,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: avatarPath != null && avatarPath.isNotEmpty
|
child: avatarPath != null && avatarPath.isNotEmpty
|
||||||
? Image.file(File(avatarPath), fit: BoxFit.cover,
|
? Image.file(File(avatarPath), fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) =>
|
errorBuilder: (_, __, ___) =>
|
||||||
const Icon(Icons.person_outline, size: 26, color: Color(0xFFBBBBBB)))
|
Icon(Icons.person_outline, size: 26, color: colors.onSurface.withValues(alpha: 0.3)))
|
||||||
: const Icon(Icons.person_outline, size: 26, color: Color(0xFFBBBBBB)),
|
: Icon(Icons.person_outline, size: 26, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 14),
|
const SizedBox(width: 14),
|
||||||
// 名称 + 座右铭
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(nickname, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
Text(nickname, style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(motto, maxLines: 1, overflow: TextOverflow.ellipsis,
|
Text(motto, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Divider(height: 1, color: Color(0xFFF0F0F0)),
|
Divider(height: 1, color: colors.outlineVariant),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
// 统计数字
|
|
||||||
_buildProfileStatRow(Icons.movie_outlined, movieCount, '观影'),
|
_buildProfileStatRow(Icons.movie_outlined, movieCount, '观影'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildProfileStatRow(Icons.menu_book_outlined, bookCount, '阅读'),
|
_buildProfileStatRow(Icons.menu_book_outlined, bookCount, '阅读'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildProfileStatRow(Icons.note_outlined, noteCount, '笔记'),
|
_buildProfileStatRow(Icons.note_outlined, noteCount, '笔记'),
|
||||||
|
|
||||||
// 设置入口
|
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
const Divider(height: 1, color: Color(0xFFF0F0F0)),
|
Divider(height: 1, color: colors.outlineVariant),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsPage()));
|
Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsPage()));
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: const Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(top: 14),
|
padding: const EdgeInsets.only(top: 14),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.settings_outlined, size: 16, color: Color(0xFF999999)),
|
Icon(Icons.settings_outlined, size: 16, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('设置', style: TextStyle(fontSize: 13, color: Color(0xFF888888))),
|
Text('设置', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||||
Spacer(),
|
const Spacer(),
|
||||||
Icon(Icons.chevron_right, size: 14, color: Color(0xFFD0D0D0)),
|
Icon(Icons.chevron_right, size: 14, color: colors.onSurface.withValues(alpha: 0.2)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -172,13 +154,14 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProfileStatRow(IconData icon, int count, String label) {
|
Widget _buildProfileStatRow(IconData icon, int count, String label) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 16, color: const Color(0xFF888888)),
|
Icon(icon, size: 16, color: colors.onSurface.withValues(alpha: 0.5)),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF888888))),
|
Text(label, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(_formatCount(count), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
Text(_formatCount(count), style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -192,10 +175,11 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
// ─── 功能入口卡片 ────────────────────────────────────────────────────
|
// ─── 功能入口卡片 ────────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildToolsCard(BuildContext context) {
|
Widget _buildToolsCard(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -204,12 +188,12 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage()));
|
Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage()));
|
||||||
}, topRounded: true),
|
}, topRounded: true),
|
||||||
const Divider(height: 1, indent: 52, endIndent: 20, color: Color(0xFFF0F0F0)),
|
Divider(height: 1, indent: 52, endIndent: 20, color: colors.outlineVariant),
|
||||||
_buildToolItem(Icons.label_outline, '标签管理', () {
|
_buildToolItem(Icons.label_outline, '标签管理', () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const TagManagementPage()));
|
Navigator.push(context, MaterialPageRoute(builder: (_) => const TagManagementPage()));
|
||||||
}),
|
}),
|
||||||
const Divider(height: 1, indent: 52, endIndent: 20, color: Color(0xFFF0F0F0)),
|
Divider(height: 1, indent: 52, endIndent: 20, color: colors.outlineVariant),
|
||||||
_buildToolItem(Icons.description_outlined, 'MD阅读', () {
|
_buildToolItem(Icons.description_outlined, 'MD阅读', () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const MdReaderTabPage()));
|
Navigator.push(context, MaterialPageRoute(builder: (_) => const MdReaderTabPage()));
|
||||||
@@ -220,6 +204,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildToolItem(IconData icon, String title, VoidCallback onTap, {bool topRounded = false, bool bottomRounded = false}) {
|
Widget _buildToolItem(IconData icon, String title, VoidCallback onTap, {bool topRounded = false, bool bottomRounded = false}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
@@ -232,21 +217,22 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20),
|
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 20, color: const Color(0xFF555555)),
|
Icon(icon, size: 20, color: colors.onSurface.withValues(alpha: 0.7)),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(child: Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))),
|
Expanded(child: Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface))),
|
||||||
const Icon(Icons.chevron_right, size: 16, color: Color(0xFFD0D0D0)),
|
Icon(Icons.chevron_right, size: 16, color: colors.onSurface.withValues(alpha: 0.2)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 热力图(保持现状) ──────────────────────────────────────────────
|
// ─── 热力图 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildCalendarSection(BuildContext context) {
|
Widget _buildCalendarSection(BuildContext context) {
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final Map<DateTime, int> dailyCounts = {};
|
final Map<DateTime, int> dailyCounts = {};
|
||||||
for (final movie in provider.movies.where((m) => !m.isDeleted)) {
|
for (final movie in provider.movies.where((m) => !m.isDeleted)) {
|
||||||
final date = DateTime(movie.createdAt.year, movie.createdAt.month, movie.createdAt.day);
|
final date = DateTime(movie.createdAt.year, movie.createdAt.month, movie.createdAt.day);
|
||||||
@@ -299,15 +285,15 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
padding: const EdgeInsets.all(18),
|
padding: const EdgeInsets.all(18),
|
||||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
|
decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(16)),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.calendar_today, size: 14, color: Color(0xFF999999)),
|
Icon(Icons.calendar_today, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('热力图', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))),
|
Text('热力图', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
@@ -324,7 +310,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
final label = keepWeeks.contains(week) ? monthLabels[week] : null;
|
final label = keepWeeks.contains(week) ? monthLabels[week] : null;
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: cellSize + cellGap,
|
width: cellSize + cellGap,
|
||||||
child: label != null ? Text(label, style: const TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))) : null,
|
child: label != null ? Text(label, style: TextStyle(fontSize: 9, color: colors.onSurface.withValues(alpha: 0.3))) : null,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -347,7 +333,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
const Text('少', style: TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))),
|
Text('少', style: TextStyle(fontSize: 9, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
const SizedBox(width: 3),
|
const SizedBox(width: 3),
|
||||||
_legendCell(const Color(0xFFF0F0F0)),
|
_legendCell(const Color(0xFFF0F0F0)),
|
||||||
_legendCell(const Color(0xFFC8E6C9)),
|
_legendCell(const Color(0xFFC8E6C9)),
|
||||||
@@ -355,7 +341,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
_legendCell(const Color(0xFF2E7D32)),
|
_legendCell(const Color(0xFF2E7D32)),
|
||||||
_legendCell(const Color(0xFF1B5E20)),
|
_legendCell(const Color(0xFF1B5E20)),
|
||||||
const SizedBox(width: 3),
|
const SizedBox(width: 3),
|
||||||
const Text('多', style: TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))),
|
Text('多', style: TextStyle(fontSize: 9, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -384,21 +370,22 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
Widget _buildRecentSection(BuildContext context) {
|
Widget _buildRecentSection(BuildContext context) {
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
final recent = _getRecentItems(provider);
|
final recent = _getRecentItems(provider);
|
||||||
if (recent.isEmpty) return const SizedBox.shrink();
|
if (recent.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
padding: const EdgeInsets.all(18),
|
padding: const EdgeInsets.all(18),
|
||||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
|
decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(16)),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.schedule, size: 14, color: Color(0xFF999999)),
|
Icon(Icons.schedule, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('最近添加', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))),
|
Text('最近添加', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
@@ -408,15 +395,15 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
item.type == 'movie' ? Icons.movie_outlined : item.type == 'book' ? Icons.menu_book_outlined : Icons.note_outlined,
|
item.type == 'movie' ? Icons.movie_outlined : item.type == 'book' ? Icons.menu_book_outlined : Icons.note_outlined,
|
||||||
size: 14, color: const Color(0xFFBBBBBB),
|
size: 14, color: colors.onSurface.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(item.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
child: Text(item.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 13, color: Color(0xFF444444))),
|
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.75))),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(_recentTimeAgo(item.date), style: const TextStyle(fontSize: 10, color: Color(0xFFCCCCCC))),
|
Text(_recentTimeAgo(item.date), style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是 http 开头,直接当网络图片
|
|
||||||
if (widget.path!.startsWith('http')) {
|
if (widget.path!.startsWith('http')) {
|
||||||
_useNetwork = true;
|
_useNetwork = true;
|
||||||
_imageUrl = widget.path;
|
_imageUrl = widget.path;
|
||||||
@@ -60,7 +59,6 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本地文件存在就直接显示
|
|
||||||
final file = File(widget.path!);
|
final file = File(widget.path!);
|
||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
setState(() => _loaded = true);
|
setState(() => _loaded = true);
|
||||||
@@ -68,7 +66,6 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本地不存在,尝试服务端 URL
|
|
||||||
if (ServerDataService.isActive) {
|
if (ServerDataService.isActive) {
|
||||||
try {
|
try {
|
||||||
final url = await ServerDataService.toImageUrl(widget.path!);
|
final url = await ServerDataService.toImageUrl(widget.path!);
|
||||||
@@ -105,13 +102,14 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
if (_error) {
|
if (_error) {
|
||||||
return widget.errorWidget ??
|
return widget.errorWidget ??
|
||||||
Container(
|
Container(
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
child: const Icon(Icons.broken_image_outlined, size: 24, color: Color(0xFFCCCCCC)),
|
child: Icon(Icons.broken_image_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!_loaded) {
|
if (!_loaded) {
|
||||||
@@ -119,7 +117,7 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
|||||||
Container(
|
Container(
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return FadeTransition(
|
return FadeTransition(
|
||||||
@@ -136,8 +134,8 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
|||||||
Container(
|
Container(
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
child: const Icon(Icons.broken_image_outlined, size: 24, color: Color(0xFFCCCCCC)),
|
child: Icon(Icons.broken_image_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -150,8 +148,8 @@ class _FadeInLocalImageState extends State<FadeInLocalImage>
|
|||||||
Container(
|
Container(
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
child: const Icon(Icons.broken_image_outlined, size: 24, color: Color(0xFFCCCCCC)),
|
child: Icon(Icons.broken_image_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class MovieListItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pushNamed(context, '/movie-detail', arguments: movie);
|
Navigator.pushNamed(context, '/movie-detail', arguments: movie);
|
||||||
@@ -23,28 +24,21 @@ class MovieListItem extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 海报
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildPoster(),
|
child: _buildPoster(colors),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// 影视名称
|
|
||||||
Text(
|
Text(
|
||||||
movie.title,
|
movie.title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1A1A1A),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
// 评分
|
|
||||||
if (movie.rating != null)
|
if (movie.rating != null)
|
||||||
AnimatedStarRating(rating: movie.rating!, starSize: 12, showNumber: true)
|
AnimatedStarRating(rating: movie.rating!, starSize: 12, showNumber: true)
|
||||||
else
|
else
|
||||||
@@ -53,45 +47,45 @@ class MovieListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建海报
|
Widget _buildPoster(ColorScheme colors) {
|
||||||
Widget _buildPoster() {
|
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5),
|
border: Border.all(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: FadeInLocalImage(
|
child: FadeInLocalImage(
|
||||||
path: movie.posterPath,
|
path: movie.posterPath,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: const Center(child: Icon(Icons.movie_outlined, size: 24, color: Color(0xFFCCCCCC))),
|
placeholder: Center(child: Icon(Icons.movie_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||||
errorWidget: const Center(child: Icon(Icons.movie_outlined, size: 24, color: Color(0xFFCCCCCC))),
|
errorWidget: Center(child: Icon(Icons.movie_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示删除确认对话框
|
|
||||||
void _showDeleteDialog(BuildContext context) {
|
void _showDeleteDialog(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'确认删除',
|
'确认删除',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
content: Text(
|
content: Text(
|
||||||
'确定要删除《${movie.title}》吗?删除后可在回收站恢复。',
|
'确定要删除《${movie.title}》吗?删除后可在回收站恢复。',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -99,7 +93,7 @@ class MovieListItem extends StatelessWidget {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFF666666),
|
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
child: const Text('取消'),
|
child: const Text('取消'),
|
||||||
@@ -111,8 +105,8 @@ class MovieListItem extends StatelessWidget {
|
|||||||
ToastUtil.show(context, '已删除');
|
ToastUtil.show(context, '已删除');
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: colors.error,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colors.onError,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
|||||||
@@ -8,20 +8,21 @@ class MovieStatusBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
|
bottom: BorderSide(color: colors.outline, width: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
),
|
),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
@@ -42,7 +43,7 @@ class MovieStatusBar extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(3),
|
padding: const EdgeInsets.all(3),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1A1A),
|
color: colors.primary,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -58,18 +59,21 @@ class MovieStatusBar extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_buildTab(
|
_buildTab(
|
||||||
|
colors: colors,
|
||||||
label: '已看',
|
label: '已看',
|
||||||
icon: Icons.check_circle_outline,
|
icon: Icons.check_circle_outline,
|
||||||
isSelected: provider.movieStatusIndex == 0,
|
isSelected: provider.movieStatusIndex == 0,
|
||||||
onTap: () => provider.setMovieStatusIndex(0),
|
onTap: () => provider.setMovieStatusIndex(0),
|
||||||
),
|
),
|
||||||
_buildTab(
|
_buildTab(
|
||||||
|
colors: colors,
|
||||||
label: '在看',
|
label: '在看',
|
||||||
icon: Icons.play_circle_outline,
|
icon: Icons.play_circle_outline,
|
||||||
isSelected: provider.movieStatusIndex == 1,
|
isSelected: provider.movieStatusIndex == 1,
|
||||||
onTap: () => provider.setMovieStatusIndex(1),
|
onTap: () => provider.setMovieStatusIndex(1),
|
||||||
),
|
),
|
||||||
_buildTab(
|
_buildTab(
|
||||||
|
colors: colors,
|
||||||
label: '想看',
|
label: '想看',
|
||||||
icon: Icons.bookmark_outline,
|
icon: Icons.bookmark_outline,
|
||||||
isSelected: provider.movieStatusIndex == 2,
|
isSelected: provider.movieStatusIndex == 2,
|
||||||
@@ -89,6 +93,7 @@ class MovieStatusBar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTab({
|
Widget _buildTab({
|
||||||
|
required ColorScheme colors,
|
||||||
required String label,
|
required String label,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required bool isSelected,
|
required bool isSelected,
|
||||||
@@ -108,7 +113,7 @@ class MovieStatusBar extends StatelessWidget {
|
|||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: isSelected ? Colors.white : const Color(0xFF666666),
|
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
@@ -116,7 +121,7 @@ class MovieStatusBar extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||||
color: isSelected ? Colors.white : const Color(0xFF666666),
|
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -14,14 +14,12 @@ class NoteListItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// 使用 RepaintBoundary 减少重绘
|
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
child: _NoteListItemContent(note: note),
|
child: _NoteListItemContent(note: note),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 笔记列表项内容 - 分离出来便于优化
|
|
||||||
class _NoteListItemContent extends StatelessWidget {
|
class _NoteListItemContent extends StatelessWidget {
|
||||||
final Note note;
|
final Note note;
|
||||||
|
|
||||||
@@ -29,10 +27,10 @@ class _NoteListItemContent extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) async {
|
Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) async {
|
||||||
// 返回时刷新笔记列表
|
|
||||||
await context.read<AppProvider>().loadNotes();
|
await context.read<AppProvider>().loadNotes();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -41,40 +39,37 @@ class _NoteListItemContent extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFAFA),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// 顶部:时间 + MD标记
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
// 时间
|
|
||||||
Text(
|
Text(
|
||||||
_formatDateCached(note.updatedAt),
|
_formatDateCached(note.updatedAt),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
// MD标记
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(3),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'MD',
|
'MD',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF999999),
|
color: colors.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -83,14 +78,13 @@ class _NoteListItemContent extends StatelessWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
// 标题
|
|
||||||
if (note.title.isNotEmpty) ...[
|
if (note.title.isNotEmpty) ...[
|
||||||
Text(
|
Text(
|
||||||
note.title,
|
note.title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1A1A1A),
|
color: colors.onSurface,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
@@ -99,20 +93,18 @@ class _NoteListItemContent extends StatelessWidget {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
],
|
],
|
||||||
|
|
||||||
// 内容摘要(去除Markdown标记),内容为空则不显示
|
|
||||||
if (_collapseBlankLines(_cleanMarkdown(note.content).trim()).isNotEmpty)
|
if (_collapseBlankLines(_cleanMarkdown(note.content).trim()).isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
_collapseBlankLines(_cleanMarkdown(note.content).trim()),
|
_collapseBlankLines(_cleanMarkdown(note.content).trim()),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
|
||||||
// 底部标签
|
|
||||||
if (note.tags.isNotEmpty) ...[
|
if (note.tags.isNotEmpty) ...[
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Wrap(
|
Wrap(
|
||||||
@@ -122,15 +114,15 @@ class _NoteListItemContent extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
tag,
|
tag,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -138,10 +130,9 @@ class _NoteListItemContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
// 图片预览
|
|
||||||
if (note.images.isNotEmpty) ...[
|
if (note.images.isNotEmpty) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildImagePreviewRow(),
|
_buildImagePreviewRow(colors),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -149,8 +140,7 @@ class _NoteListItemContent extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 图片预览行
|
Widget _buildImagePreviewRow(ColorScheme colors) {
|
||||||
Widget _buildImagePreviewRow() {
|
|
||||||
final images = note.images;
|
final images = note.images;
|
||||||
final count = images.length.clamp(0, 3);
|
final count = images.length.clamp(0, 3);
|
||||||
return Row(
|
return Row(
|
||||||
@@ -162,14 +152,14 @@ class _NoteListItemContent extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.only(right: 6),
|
margin: const EdgeInsets.only(right: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5),
|
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: FadeInLocalImage(
|
child: FadeInLocalImage(
|
||||||
path: images[i],
|
path: images[i],
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorWidget: Container(
|
errorWidget: Container(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -178,13 +168,13 @@ class _NoteListItemContent extends StatelessWidget {
|
|||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF5F5F5),
|
color: colors.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'+${images.length - 3}',
|
'+${images.length - 3}',
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFF999999), fontWeight: FontWeight.w500),
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4), fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -192,45 +182,44 @@ class _NoteListItemContent extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 清理 Markdown 标记,提取纯文本
|
|
||||||
String _cleanMarkdown(String text) {
|
String _cleanMarkdown(String text) {
|
||||||
return text
|
return text
|
||||||
.replaceAll(RegExp(r'^#+\s+', multiLine: true), '') // 标题
|
.replaceAll(RegExp(r'^#+\s+', multiLine: true), '')
|
||||||
.replaceAll(RegExp(r'\*\*(.+?)\*\*'), r'$1') // 粗体
|
.replaceAll(RegExp(r'\*\*(.+?)\*\*'), r'$1')
|
||||||
.replaceAll(RegExp(r'\*(.+?)\*'), r'$1') // 斜体
|
.replaceAll(RegExp(r'\*(.+?)\*'), r'$1')
|
||||||
.replaceAll(RegExp(r'`(.+?)`'), r'$1') // 行内代码
|
.replaceAll(RegExp(r'`(.+?)`'), r'$1')
|
||||||
.replaceAll(RegExp(r'^\s*[-*+]\s', multiLine: true), '') // 列表
|
.replaceAll(RegExp(r'^\s*[-*+]\s', multiLine: true), '')
|
||||||
.replaceAll(RegExp(r'^\s*>\s', multiLine: true), '') // 引用
|
.replaceAll(RegExp(r'^\s*>\s', multiLine: true), '')
|
||||||
.replaceAll(RegExp(r'\[([^\]]+)\]\([^)]+\)'), r'$1') // 链接
|
.replaceAll(RegExp(r'\[([^\]]+)\]\([^)]+\)'), r'$1')
|
||||||
.replaceAll(RegExp(r'!\[([^\]]*)\]\([^)]+\)'), '') // 图片
|
.replaceAll(RegExp(r'!\[([^\]]*)\]\([^)]+\)'), '')
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 合并连续空行为单行
|
|
||||||
String _collapseBlankLines(String text) {
|
String _collapseBlankLines(String text) {
|
||||||
return text.replaceAll(RegExp(r'\n\s*\n+'), '\n');
|
return text.replaceAll(RegExp(r'\n\s*\n+'), '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示删除确认对话框
|
|
||||||
void _showDeleteDialog(BuildContext context) {
|
void _showDeleteDialog(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'确认删除',
|
'确认删除',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
content: const Text(
|
content: Text(
|
||||||
'确定要删除这条笔记吗?删除后可在回收站恢复。',
|
'确定要删除这条笔记吗?删除后可在回收站恢复。',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF666666),
|
color: colors.onSurface.withValues(alpha: 0.6),
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -238,7 +227,7 @@ class _NoteListItemContent extends StatelessWidget {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFF666666),
|
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
child: const Text('取消'),
|
child: const Text('取消'),
|
||||||
@@ -250,8 +239,8 @@ class _NoteListItemContent extends StatelessWidget {
|
|||||||
ToastUtil.show(context, '已删除');
|
ToastUtil.show(context, '已删除');
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: colors.error,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colors.onError,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -267,28 +256,24 @@ class _NoteListItemContent extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 日期格式化缓存
|
|
||||||
final Map<DateTime, String> _dateFormatCache = {};
|
final Map<DateTime, String> _dateFormatCache = {};
|
||||||
|
|
||||||
/// 格式化日期(带缓存)
|
|
||||||
String _formatDateCached(DateTime date) {
|
String _formatDateCached(DateTime date) {
|
||||||
// 使用日期部分作为缓存键(忽略时分秒)
|
|
||||||
final cacheKey = DateTime(date.year, date.month, date.day);
|
final cacheKey = DateTime(date.year, date.month, date.day);
|
||||||
|
|
||||||
if (_dateFormatCache.containsKey(cacheKey)) {
|
if (_dateFormatCache.containsKey(cacheKey)) {
|
||||||
return _dateFormatCache[cacheKey]!;
|
return _dateFormatCache[cacheKey]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = _formatDate(date);
|
final result = _formatDate(date);
|
||||||
_dateFormatCache[cacheKey] = result;
|
_dateFormatCache[cacheKey] = result;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 格式化日期
|
|
||||||
String _formatDate(DateTime date) {
|
String _formatDate(DateTime date) {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final difference = now.difference(date);
|
final difference = now.difference(date);
|
||||||
|
|
||||||
if (difference.inDays == 0) {
|
if (difference.inDays == 0) {
|
||||||
if (difference.inHours == 0) {
|
if (difference.inHours == 0) {
|
||||||
if (difference.inMinutes == 0) {
|
if (difference.inMinutes == 0) {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class _ShimmerSkeletonState extends State<ShimmerSkeleton>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: _animation,
|
animation: _animation,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
@@ -49,7 +50,7 @@ class _ShimmerSkeletonState extends State<ShimmerSkeleton>
|
|||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFE0E0E0).withValues(alpha: _animation.value),
|
color: colors.surfaceContainerHighest.withValues(alpha: _animation.value),
|
||||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -134,6 +135,7 @@ class NoteSkeletonList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 100),
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 100),
|
||||||
itemCount: 6,
|
itemCount: 6,
|
||||||
@@ -141,7 +143,7 @@ class NoteSkeletonList extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F8F8),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Column(
|
child: const Column(
|
||||||
|
|||||||
Reference in New Issue
Block a user