generated from dellevin/template
我的界面优化
This commit is contained in:
@@ -11,6 +11,7 @@ import '../../utils/user_prefs.dart';
|
|||||||
import '../../utils/responsive.dart';
|
import '../../utils/responsive.dart';
|
||||||
import '../../utils/toast_util.dart';
|
import '../../utils/toast_util.dart';
|
||||||
import '../../utils/image_path_helper.dart';
|
import '../../utils/image_path_helper.dart';
|
||||||
|
import '../../utils/excel_exporter.dart';
|
||||||
import '../settings/recycle_bin_page.dart';
|
import '../settings/recycle_bin_page.dart';
|
||||||
import '../sync/backup_page.dart';
|
import '../sync/backup_page.dart';
|
||||||
import '../../widgets/fade_in_local_image.dart';
|
import '../../widgets/fade_in_local_image.dart';
|
||||||
@@ -19,6 +20,7 @@ import '../settings/tag_management_page.dart';
|
|||||||
import '../explore/stroll_page.dart';
|
import '../explore/stroll_page.dart';
|
||||||
import '../sync/cloud_sync_page.dart';
|
import '../sync/cloud_sync_page.dart';
|
||||||
import 'settings_page.dart';
|
import 'settings_page.dart';
|
||||||
|
import 'watchlist_page.dart';
|
||||||
|
|
||||||
/// 个人中心页面
|
/// 个人中心页面
|
||||||
class ProfilePage extends StatefulWidget {
|
class ProfilePage extends StatefulWidget {
|
||||||
@@ -36,7 +38,7 @@ class _ProfilePageState extends State<ProfilePage> with RouteAware {
|
|||||||
String _motto = '好运不会眷顾一无所有之人。';
|
String _motto = '好运不会眷顾一无所有之人。';
|
||||||
String? _avatarPath;
|
String? _avatarPath;
|
||||||
|
|
||||||
// 我的模块切换索引 (0=影视, 1=阅读, 2=笔记, 3=游戏)
|
// 我的模块切换索引 (0=影视, 1=阅读, 2=游戏, 3=笔记)
|
||||||
int _myModuleIndex = 0;
|
int _myModuleIndex = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -107,6 +109,8 @@ class _ProfilePageState extends State<ProfilePage> with RouteAware {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildMyModule(movies, books, notes, games),
|
_buildMyModule(movies, books, notes, games),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
_buildWatchlist(movies, books, games),
|
||||||
|
const SizedBox(height: 20),
|
||||||
_buildTagsSection(movies, books, notes),
|
_buildTagsSection(movies, books, notes),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildToolsGrid(context),
|
_buildToolsGrid(context),
|
||||||
@@ -294,8 +298,8 @@ class _ProfilePageState extends State<ProfilePage> with RouteAware {
|
|||||||
static const _moduleDefs = <(String, IconData)>[
|
static const _moduleDefs = <(String, IconData)>[
|
||||||
('影视', Icons.movie_outlined),
|
('影视', Icons.movie_outlined),
|
||||||
('阅读', Icons.menu_book_outlined),
|
('阅读', Icons.menu_book_outlined),
|
||||||
('笔记', Icons.sticky_note_2_outlined),
|
|
||||||
('游戏', Icons.sports_esports_outlined),
|
('游戏', Icons.sports_esports_outlined),
|
||||||
|
('笔记', Icons.sticky_note_2_outlined),
|
||||||
];
|
];
|
||||||
|
|
||||||
List<(String, IconData)> get _visibleModules {
|
List<(String, IconData)> get _visibleModules {
|
||||||
@@ -660,6 +664,174 @@ class _ProfilePageState extends State<ProfilePage> with RouteAware {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 想看清单 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildWatchlist(List<Movie> movies, List<Book> books, List<Game> games) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
final items = <_WatchlistItem>[];
|
||||||
|
if (_userPrefs.showMovieTab) {
|
||||||
|
for (final m in movies.where((m) => m.status == 'want_to_watch')) {
|
||||||
|
items.add(_WatchlistItem(
|
||||||
|
title: m.title,
|
||||||
|
imagePath: m.posterPath,
|
||||||
|
type: 'movie',
|
||||||
|
createdAt: m.createdAt,
|
||||||
|
onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: m),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_userPrefs.showBookTab) {
|
||||||
|
for (final b in books.where((b) => b.status == 'want_to_read')) {
|
||||||
|
items.add(_WatchlistItem(
|
||||||
|
title: b.title,
|
||||||
|
imagePath: b.coverPath,
|
||||||
|
type: 'book',
|
||||||
|
createdAt: b.createdAt,
|
||||||
|
onTap: () => Navigator.pushNamed(context, '/book-detail', arguments: b),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_userPrefs.showGameTab) {
|
||||||
|
for (final g in games.where((g) => g.status == 'want_to_play')) {
|
||||||
|
items.add(_WatchlistItem(
|
||||||
|
title: g.title,
|
||||||
|
imagePath: g.coverPath,
|
||||||
|
type: 'game',
|
||||||
|
createdAt: g.createdAt,
|
||||||
|
onTap: () => Navigator.pushNamed(context, '/game-detail', arguments: g),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
|
|
||||||
|
final typeIcon = <String, IconData>{
|
||||||
|
'movie': Icons.movie_outlined,
|
||||||
|
'book': Icons.menu_book_outlined,
|
||||||
|
'game': Icons.sports_esports_outlined,
|
||||||
|
};
|
||||||
|
final typeColor = <String, Color>{
|
||||||
|
'movie': const Color(0xFF2563EB),
|
||||||
|
'book': const Color(0xFF16A34A),
|
||||||
|
'game': const Color(0xFFEA580C),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text('想看清单',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface)),
|
||||||
|
const Spacer(),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const WatchlistPage())),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('全部',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
|
Icon(Icons.chevron_right,
|
||||||
|
size: 16,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.3)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
if (items.isEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||||
|
child: Center(
|
||||||
|
child: Text('暂无想看记录',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.3)))),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SizedBox(
|
||||||
|
height: 120,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
itemCount: items.take(20).length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
final item = items[i];
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: item.onTap,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 78,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 78,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: item.imagePath != null && item.imagePath!.isNotEmpty
|
||||||
|
? FadeInLocalImage(
|
||||||
|
path: item.imagePath,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorWidget: Icon(Icons.image_outlined,
|
||||||
|
size: 20,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.2)))
|
||||||
|
: Icon(Icons.image_outlined,
|
||||||
|
size: 20,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.2)),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 4,
|
||||||
|
left: 4,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: typeColor[item.type]!,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Icon(typeIcon[item.type],
|
||||||
|
size: 10, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(item.title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildEmptyHint(String text) {
|
Widget _buildEmptyHint(String text) {
|
||||||
final colors = Theme.of(context).colorScheme;
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -777,18 +949,19 @@ class _ProfilePageState extends State<ProfilePage> with RouteAware {
|
|||||||
context, MaterialPageRoute(builder: (_) => const StatisticsPage()))
|
context, MaterialPageRoute(builder: (_) => const StatisticsPage()))
|
||||||
),
|
),
|
||||||
(Icons.backup_outlined, '备份', () => _showBackupOptions(context)),
|
(Icons.backup_outlined, '备份', () => _showBackupOptions(context)),
|
||||||
(
|
(Icons.ios_share_outlined, '导出', () => _showExportOptions(context)),
|
||||||
Icons.delete_outline,
|
|
||||||
'回收',
|
|
||||||
() => Navigator.push(
|
|
||||||
context, MaterialPageRoute(builder: (_) => const RecycleBinPage()))
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Icons.settings_outlined,
|
Icons.settings_outlined,
|
||||||
'设置',
|
'设置',
|
||||||
() => Navigator.push(
|
() => Navigator.push(
|
||||||
context, MaterialPageRoute(builder: (_) => const SettingsPage()))
|
context, MaterialPageRoute(builder: (_) => const SettingsPage()))
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
Icons.delete_outline,
|
||||||
|
'回收',
|
||||||
|
() => Navigator.push(
|
||||||
|
context, MaterialPageRoute(builder: (_) => const RecycleBinPage()))
|
||||||
|
),
|
||||||
(Icons.feedback_outlined, '反馈', () => _showFeedbackDialog(context)),
|
(Icons.feedback_outlined, '反馈', () => _showFeedbackDialog(context)),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1212,4 +1385,136 @@ class _ProfilePageState extends State<ProfilePage> with RouteAware {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showExportOptions(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
final provider = context.read<AppProvider>();
|
||||||
|
final userPrefs = UserPrefs();
|
||||||
|
|
||||||
|
final options = <(String, IconData, int)>[];
|
||||||
|
if (userPrefs.showMovieTab) {
|
||||||
|
options.add(('影视', Icons.movie_outlined, provider.movies.where((m) => !m.isDeleted).length));
|
||||||
|
}
|
||||||
|
if (userPrefs.showBookTab) {
|
||||||
|
options.add(('阅读', Icons.menu_book_outlined, provider.books.where((b) => !b.isDeleted).length));
|
||||||
|
}
|
||||||
|
if (userPrefs.showGameTab) {
|
||||||
|
options.add(('游戏', Icons.sports_esports_outlined, provider.games.where((g) => !g.isDeleted).length));
|
||||||
|
}
|
||||||
|
options.add(('笔记', Icons.sticky_note_2_outlined, provider.notes.where((n) => !n.isDeleted).length));
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||||
|
builder: (ctx) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 36,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.15),
|
||||||
|
borderRadius: BorderRadius.circular(2))),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text('选择导出类型',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface))),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text('导出为 Excel (.xlsx) 格式',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.4)))),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
for (final (label, icon, count) in options) ...[
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: Icon(icon,
|
||||||
|
size: 18,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
|
title: Text(label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: colors.onSurface)),
|
||||||
|
subtitle: Text('$count 条记录',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
|
trailing: Icon(Icons.chevron_right,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_doExport(context, label);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (label != options.last.$1)
|
||||||
|
Divider(height: 0.5, color: colors.outlineVariant),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _doExport(BuildContext context, String type) async {
|
||||||
|
final provider = context.read<AppProvider>();
|
||||||
|
try {
|
||||||
|
File file;
|
||||||
|
switch (type) {
|
||||||
|
case '影视':
|
||||||
|
file = await ExcelExporter.exportMovies(
|
||||||
|
provider.movies.where((m) => !m.isDeleted).toList());
|
||||||
|
break;
|
||||||
|
case '阅读':
|
||||||
|
file = await ExcelExporter.exportBooks(
|
||||||
|
provider.books.where((b) => !b.isDeleted).toList());
|
||||||
|
break;
|
||||||
|
case '游戏':
|
||||||
|
file = await ExcelExporter.exportGames(
|
||||||
|
provider.games.where((g) => !g.isDeleted).toList());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
file = await ExcelExporter.exportNotes(
|
||||||
|
provider.notes.where((n) => !n.isDeleted).toList());
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ToastUtil.show(context, '已导出到 ${file.path}');
|
||||||
|
} catch (e) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ToastUtil.show(context, '导出失败:$e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WatchlistItem {
|
||||||
|
final String title;
|
||||||
|
final String? imagePath;
|
||||||
|
final String type; // movie / book / game
|
||||||
|
final DateTime createdAt;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _WatchlistItem({
|
||||||
|
required this.title,
|
||||||
|
this.imagePath,
|
||||||
|
required this.type,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
424
lib/pages/profile/watchlist_page.dart
Normal file
424
lib/pages/profile/watchlist_page.dart
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../../providers/app_provider.dart';
|
||||||
|
import '../../utils/user_prefs.dart';
|
||||||
|
import '../../widgets/fade_in_local_image.dart';
|
||||||
|
|
||||||
|
/// 想看清单总览页面 - 汇总影视/书籍/游戏的想看记录
|
||||||
|
class WatchlistPage extends StatefulWidget {
|
||||||
|
const WatchlistPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<WatchlistPage> createState() => _WatchlistPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WatchlistPageState extends State<WatchlistPage> {
|
||||||
|
// 筛选索引:0=全部, 1=影视, 2=阅读, 3=游戏
|
||||||
|
int _filterIndex = 0;
|
||||||
|
|
||||||
|
static const _filterDefs = <(String, String)>[
|
||||||
|
('全部', ''),
|
||||||
|
('影视', 'movie'),
|
||||||
|
('阅读', 'book'),
|
||||||
|
('游戏', 'game'),
|
||||||
|
];
|
||||||
|
|
||||||
|
static const _typeLabel = <String, String>{
|
||||||
|
'movie': '影视',
|
||||||
|
'book': '阅读',
|
||||||
|
'game': '游戏',
|
||||||
|
};
|
||||||
|
static const _typeIcon = <String, IconData>{
|
||||||
|
'movie': Icons.movie_outlined,
|
||||||
|
'book': Icons.menu_book_outlined,
|
||||||
|
'game': Icons.sports_esports_outlined,
|
||||||
|
};
|
||||||
|
static const _typeColor = <String, Color>{
|
||||||
|
'movie': Color(0xFF2563EB),
|
||||||
|
'book': Color(0xFF16A34A),
|
||||||
|
'game': Color(0xFFEA580C),
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
final provider = context.watch<AppProvider>();
|
||||||
|
final userPrefs = UserPrefs();
|
||||||
|
|
||||||
|
final movies = provider.movies.where((m) => !m.isDeleted).toList();
|
||||||
|
final books = provider.books.where((b) => !b.isDeleted).toList();
|
||||||
|
final games = provider.games.where((g) => !g.isDeleted).toList();
|
||||||
|
|
||||||
|
final items = <_WatchlistEntry>[];
|
||||||
|
if (userPrefs.showMovieTab) {
|
||||||
|
for (final m in movies.where((m) => m.status == 'want_to_watch')) {
|
||||||
|
items.add(_WatchlistEntry(
|
||||||
|
title: m.title,
|
||||||
|
imagePath: m.posterPath,
|
||||||
|
type: 'movie',
|
||||||
|
createdAt: m.createdAt,
|
||||||
|
onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: m),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userPrefs.showBookTab) {
|
||||||
|
for (final b in books.where((b) => b.status == 'want_to_read')) {
|
||||||
|
items.add(_WatchlistEntry(
|
||||||
|
title: b.title,
|
||||||
|
imagePath: b.coverPath,
|
||||||
|
type: 'book',
|
||||||
|
createdAt: b.createdAt,
|
||||||
|
onTap: () => Navigator.pushNamed(context, '/book-detail', arguments: b),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userPrefs.showGameTab) {
|
||||||
|
for (final g in games.where((g) => g.status == 'want_to_play')) {
|
||||||
|
items.add(_WatchlistEntry(
|
||||||
|
title: g.title,
|
||||||
|
imagePath: g.coverPath,
|
||||||
|
type: 'game',
|
||||||
|
createdAt: g.createdAt,
|
||||||
|
onTap: () => Navigator.pushNamed(context, '/game-detail', arguments: g),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
|
|
||||||
|
// 可见筛选标签
|
||||||
|
final visibleFilters = <(String, String)>[];
|
||||||
|
visibleFilters.add(_filterDefs[0]);
|
||||||
|
if (userPrefs.showMovieTab) visibleFilters.add(_filterDefs[1]);
|
||||||
|
if (userPrefs.showBookTab) visibleFilters.add(_filterDefs[2]);
|
||||||
|
if (userPrefs.showGameTab) visibleFilters.add(_filterDefs[3]);
|
||||||
|
|
||||||
|
final (_, filterType) = visibleFilters[_filterIndex.clamp(0, visibleFilters.length - 1)];
|
||||||
|
final filtered = filterType.isEmpty
|
||||||
|
? items
|
||||||
|
: items.where((i) => i.type == filterType).toList();
|
||||||
|
|
||||||
|
// 按类型分组(仅"全部"视图分组,单类型筛选不分组)
|
||||||
|
final useGroups = filterType.isEmpty;
|
||||||
|
final groups = <_WatchlistGroup>[];
|
||||||
|
if (useGroups) {
|
||||||
|
for (final type in const ['movie', 'book', 'game']) {
|
||||||
|
final groupItems = filtered.where((i) => i.type == type).toList();
|
||||||
|
if (groupItems.isNotEmpty) {
|
||||||
|
groups.add(_WatchlistGroup(type: type, items: groupItems));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
appBar: AppBar(title: const Text('想看清单')),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: _buildSlivers(
|
||||||
|
colors: colors,
|
||||||
|
visibleFilters: visibleFilters,
|
||||||
|
filtered: filtered,
|
||||||
|
useGroups: useGroups,
|
||||||
|
groups: groups,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildSlivers({
|
||||||
|
required ColorScheme colors,
|
||||||
|
required List<(String, String)> visibleFilters,
|
||||||
|
required List<_WatchlistEntry> filtered,
|
||||||
|
required bool useGroups,
|
||||||
|
required List<_WatchlistGroup> groups,
|
||||||
|
}) {
|
||||||
|
final slivers = <Widget>[];
|
||||||
|
if (visibleFilters.length > 1) {
|
||||||
|
slivers.add(SliverPersistentHeader(
|
||||||
|
pinned: true,
|
||||||
|
delegate: _StickyFilterDelegate(
|
||||||
|
visibleFilters: visibleFilters,
|
||||||
|
filterIndex: _filterIndex,
|
||||||
|
count: filtered.length,
|
||||||
|
onTap: (i) => setState(() => _filterIndex = i),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (filtered.isEmpty) {
|
||||||
|
slivers.add(SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: _buildEmpty(colors),
|
||||||
|
));
|
||||||
|
} else if (useGroups) {
|
||||||
|
for (final g in groups) {
|
||||||
|
slivers.addAll(_buildGroupSlivers(g, colors));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slivers.add(SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 4, 16, 120),
|
||||||
|
sliver: SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(ctx, i) => _buildItemCard(colors, filtered[i]),
|
||||||
|
childCount: filtered.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return slivers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分组 slivers:小标题 + 卡片列表
|
||||||
|
List<Widget> _buildGroupSlivers(_WatchlistGroup group, ColorScheme colors) {
|
||||||
|
return [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 3,
|
||||||
|
height: 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _typeColor[group.type],
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(_typeLabel[group.type]!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.7))),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text('${group.items.length}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 4),
|
||||||
|
sliver: SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(ctx, i) => _buildItemCard(colors, group.items[i]),
|
||||||
|
childCount: group.items.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单条卡片
|
||||||
|
Widget _buildItemCard(ColorScheme colors, _WatchlistEntry item) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: item.onTap,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 封面
|
||||||
|
Container(
|
||||||
|
width: 52,
|
||||||
|
height: 74,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.08),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: item.imagePath != null && item.imagePath!.isNotEmpty
|
||||||
|
? FadeInLocalImage(
|
||||||
|
path: item.imagePath,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorWidget: Icon(Icons.image_outlined,
|
||||||
|
size: 18, color: colors.onSurface.withValues(alpha: 0.2)))
|
||||||
|
: Icon(Icons.image_outlined,
|
||||||
|
size: 18, color: colors.onSurface.withValues(alpha: 0.2)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
// 左侧竖色条
|
||||||
|
Container(
|
||||||
|
width: 3,
|
||||||
|
height: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _typeColor[item.type]!.withValues(alpha: 0.6),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
// 标题 + 类型
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(item.title,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: colors.onSurface,
|
||||||
|
height: 1.3)),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(_typeIcon[item.type],
|
||||||
|
size: 11, color: _typeColor[item.type]),
|
||||||
|
const SizedBox(width: 3),
|
||||||
|
Text(_typeLabel[item.type]!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: _typeColor[item.type])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 右侧加入时间
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(_formatRelative(item.createdAt),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmpty(ColorScheme colors) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.bookmark_border,
|
||||||
|
size: 48, color: colors.onSurface.withValues(alpha: 0.15)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text('暂无想看记录',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatRelative(DateTime d) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final diff = now.difference(d);
|
||||||
|
if (diff.inMinutes < 1) return '刚刚';
|
||||||
|
if (diff.inHours < 1) return '${diff.inMinutes}分钟前';
|
||||||
|
if (diff.inDays < 1) return '${diff.inHours}小时前';
|
||||||
|
if (diff.inDays < 30) return '${diff.inDays}天前';
|
||||||
|
if (diff.inDays < 365) return '${(diff.inDays / 30).floor()}个月前';
|
||||||
|
return '${(diff.inDays / 365).floor()}年前';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 吸顶筛选栏
|
||||||
|
class _StickyFilterDelegate extends SliverPersistentHeaderDelegate {
|
||||||
|
final List<(String, String)> visibleFilters;
|
||||||
|
final int filterIndex;
|
||||||
|
final int count;
|
||||||
|
final ValueChanged<int> onTap;
|
||||||
|
|
||||||
|
_StickyFilterDelegate({
|
||||||
|
required this.visibleFilters,
|
||||||
|
required this.filterIndex,
|
||||||
|
required this.count,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get minExtent => 52;
|
||||||
|
@override
|
||||||
|
double get maxExtent => 52;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return Container(
|
||||||
|
color: colors.surface,
|
||||||
|
height: 52,
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 10, 20, 8),
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < visibleFilters.length; i++) ...[
|
||||||
|
if (i != 0) const SizedBox(width: 8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => onTap(i),
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 14, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: i == filterIndex
|
||||||
|
? colors.primary
|
||||||
|
: colors.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Text(visibleFilters[i].$1,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight:
|
||||||
|
i == filterIndex ? FontWeight.w600 : FontWeight.w400,
|
||||||
|
color: i == filterIndex
|
||||||
|
? colors.onPrimary
|
||||||
|
: colors.onSurface.withValues(alpha: 0.6))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const Spacer(),
|
||||||
|
Text('$count项',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRebuild(covariant _StickyFilterDelegate oldDelegate) =>
|
||||||
|
filterIndex != oldDelegate.filterIndex || count != oldDelegate.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WatchlistEntry {
|
||||||
|
final String title;
|
||||||
|
final String? imagePath;
|
||||||
|
final String type; // movie / book / game
|
||||||
|
final DateTime createdAt;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _WatchlistEntry({
|
||||||
|
required this.title,
|
||||||
|
this.imagePath,
|
||||||
|
required this.type,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WatchlistGroup {
|
||||||
|
final String type;
|
||||||
|
final List<_WatchlistEntry> items;
|
||||||
|
const _WatchlistGroup({required this.type, required this.items});
|
||||||
|
}
|
||||||
195
lib/utils/excel_exporter.dart
Normal file
195
lib/utils/excel_exporter.dart
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:excel/excel.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import '../models/data_models.dart';
|
||||||
|
|
||||||
|
/// 数据导出为 Excel 文件
|
||||||
|
class ExcelExporter {
|
||||||
|
/// 导出影视数据,返回生成的文件
|
||||||
|
static Future<File> exportMovies(List<Movie> movies) async {
|
||||||
|
final excel = Excel.createExcel();
|
||||||
|
final sheet = excel['影视'];
|
||||||
|
|
||||||
|
sheet.appendRow(_row([
|
||||||
|
'名称', '类别', '状态', '评分', '导演', '编剧', '主演',
|
||||||
|
'类型', '别名', '上映时间', '观看日期', '观看次数', '时长(分钟)',
|
||||||
|
'简介', '创建时间', '更新时间',
|
||||||
|
]));
|
||||||
|
|
||||||
|
final statusMap = {'watched': '已看', 'watching': '在看', 'want_to_watch': '想看'};
|
||||||
|
final categoryMap = {
|
||||||
|
'movie': '电影', 'tv': '电视剧', 'anime': '动漫',
|
||||||
|
'variety': '综艺', 'documentary': '纪录片', 'short': '短片', 'other': '其他',
|
||||||
|
};
|
||||||
|
|
||||||
|
for (final m in movies) {
|
||||||
|
sheet.appendRow(_row([
|
||||||
|
m.title,
|
||||||
|
categoryMap[m.category] ?? m.category,
|
||||||
|
statusMap[m.status] ?? m.status,
|
||||||
|
m.rating?.toString() ?? '',
|
||||||
|
m.directors.join('、'),
|
||||||
|
m.writers.join('、'),
|
||||||
|
m.actors.join('、'),
|
||||||
|
m.genres.join('、'),
|
||||||
|
m.alternateTitles.join('、'),
|
||||||
|
_fmtDate(m.releaseDate),
|
||||||
|
_fmtDate(m.watchDate),
|
||||||
|
m.watchCount.toString(),
|
||||||
|
m.duration.toString(),
|
||||||
|
m.summary ?? '',
|
||||||
|
_fmtDateTime(m.createdAt),
|
||||||
|
_fmtDateTime(m.updatedAt),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return _save(excel, '影视');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导出阅读数据
|
||||||
|
static Future<File> exportBooks(List<Book> books) async {
|
||||||
|
final excel = Excel.createExcel();
|
||||||
|
final sheet = excel['阅读'];
|
||||||
|
|
||||||
|
sheet.appendRow(_row([
|
||||||
|
'名称', '状态', '评分', '作者', '译者', '别名', '出版社',
|
||||||
|
'类型', 'ISBN', '出版时间', '开始阅读', '读完时间', '阅读次数',
|
||||||
|
'简介', '创建时间', '更新时间',
|
||||||
|
]));
|
||||||
|
|
||||||
|
final statusMap = {'read': '已读', 'reading': '在读', 'want_to_read': '想读'};
|
||||||
|
|
||||||
|
for (final b in books) {
|
||||||
|
sheet.appendRow(_row([
|
||||||
|
b.title,
|
||||||
|
statusMap[b.status] ?? b.status,
|
||||||
|
b.rating?.toString() ?? '',
|
||||||
|
b.authors.join('、'),
|
||||||
|
b.translators.join('、'),
|
||||||
|
b.alternateTitles.join('、'),
|
||||||
|
b.publisher ?? '',
|
||||||
|
b.genres.join('、'),
|
||||||
|
b.isbn ?? '',
|
||||||
|
_fmtDate(b.publishDate),
|
||||||
|
_fmtDate(b.startDate),
|
||||||
|
_fmtDate(b.finishDate),
|
||||||
|
b.readCount.toString(),
|
||||||
|
b.summary ?? '',
|
||||||
|
_fmtDateTime(b.createdAt),
|
||||||
|
_fmtDateTime(b.updatedAt),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return _save(excel, '阅读');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导出游戏数据
|
||||||
|
static Future<File> exportGames(List<Game> games) async {
|
||||||
|
final excel = Excel.createExcel();
|
||||||
|
final sheet = excel['游戏'];
|
||||||
|
|
||||||
|
sheet.appendRow(_row([
|
||||||
|
'名称', '状态', '评分', '分类', '平台', '版本', '类型',
|
||||||
|
'游玩时长(小时)', '游玩时长(分钟)', '游玩次数', '开发者',
|
||||||
|
'发售时间', '购买平台', '购买日期', '购买价格', '简介',
|
||||||
|
'创建时间', '更新时间',
|
||||||
|
]));
|
||||||
|
|
||||||
|
final statusMap = {
|
||||||
|
'completed': '已通关', 'playing': '在玩',
|
||||||
|
'want_to_play': '想玩', 'abandoned': '弃游',
|
||||||
|
};
|
||||||
|
final categoryMap = {'digital': '数字版', 'cartridge': '卡带', 'disc': '光盘'};
|
||||||
|
|
||||||
|
for (final g in games) {
|
||||||
|
sheet.appendRow(_row([
|
||||||
|
g.title,
|
||||||
|
statusMap[g.status] ?? g.status,
|
||||||
|
g.rating?.toString() ?? '',
|
||||||
|
categoryMap[g.category] ?? g.category,
|
||||||
|
g.platforms.join('、'),
|
||||||
|
g.versions.join('、'),
|
||||||
|
g.genres.join('、'),
|
||||||
|
g.playTimeHours.toString(),
|
||||||
|
g.playTimeMinutes.toString(),
|
||||||
|
g.playCount.toString(),
|
||||||
|
g.developer.join('、'),
|
||||||
|
_fmtDate(g.releaseDate),
|
||||||
|
g.purchasePlatforms.join('、'),
|
||||||
|
_fmtDate(g.purchaseDate),
|
||||||
|
g.purchasePrice ?? '',
|
||||||
|
g.summary ?? '',
|
||||||
|
_fmtDateTime(g.createdAt),
|
||||||
|
_fmtDateTime(g.updatedAt),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return _save(excel, '游戏');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导出笔记数据
|
||||||
|
static Future<File> exportNotes(List<Note> notes) async {
|
||||||
|
final excel = Excel.createExcel();
|
||||||
|
final sheet = excel['笔记'];
|
||||||
|
|
||||||
|
sheet.appendRow(_row([
|
||||||
|
'标题', '内容', '内容类型', '标签', '图片数', '是否置顶',
|
||||||
|
'创建时间', '更新时间',
|
||||||
|
]));
|
||||||
|
|
||||||
|
for (final n in notes) {
|
||||||
|
sheet.appendRow(_row([
|
||||||
|
n.title,
|
||||||
|
n.content,
|
||||||
|
n.contentType,
|
||||||
|
n.tags.join('、'),
|
||||||
|
n.images.length.toString(),
|
||||||
|
n.isPinned ? '是' : '否',
|
||||||
|
_fmtDateTime(n.createdAt),
|
||||||
|
_fmtDateTime(n.updatedAt),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return _save(excel, '笔记');
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<CellValue?> _row(List<String> values) {
|
||||||
|
return values.map((v) => TextCellValue(v)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<File> _save(Excel excel, String name) async {
|
||||||
|
if (excel.sheets.containsKey('Sheet1')) {
|
||||||
|
excel.delete('Sheet1');
|
||||||
|
}
|
||||||
|
final fileName = '${name}_导出_${_timestamp()}.xlsx';
|
||||||
|
String filePath;
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final exportDir = Directory('/sdcard/Download/mooknote/export');
|
||||||
|
if (!await exportDir.exists()) {
|
||||||
|
await exportDir.create(recursive: true);
|
||||||
|
}
|
||||||
|
filePath = p.join(exportDir.path, fileName);
|
||||||
|
} else {
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
filePath = p.join(tempDir.path, fileName);
|
||||||
|
}
|
||||||
|
final file = File(filePath);
|
||||||
|
await file.writeAsBytes(excel.encode()!);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _fmtDate(DateTime? d) {
|
||||||
|
if (d == null) return '';
|
||||||
|
return '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _fmtDateTime(DateTime d) {
|
||||||
|
return '${_fmtDate(d)} ${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _timestamp() {
|
||||||
|
final d = DateTime.now();
|
||||||
|
return '${d.year}${d.month.toString().padLeft(2, '0')}${d.day.toString().padLeft(2, '0')}_${d.hour.toString().padLeft(2, '0')}${d.minute.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,8 +20,8 @@ import '../pages/movies/movie_form_page.dart';
|
|||||||
import '../pages/book/book_detail_page.dart';
|
import '../pages/book/book_detail_page.dart';
|
||||||
import '../pages/book/book_form_page.dart';
|
import '../pages/book/book_form_page.dart';
|
||||||
import '../pages/note/note_detail_page.dart';
|
import '../pages/note/note_detail_page.dart';
|
||||||
import '../pages/note/note_form_page.dart';
|
|
||||||
import '../pages/game/game_detail_page.dart';
|
import '../pages/game/game_detail_page.dart';
|
||||||
|
import '../pages/game/game_form_page.dart';
|
||||||
import '../pages/profile/settings_page.dart';
|
import '../pages/profile/settings_page.dart';
|
||||||
import '../models/data_models.dart';
|
import '../models/data_models.dart';
|
||||||
import 'fade_in_local_image.dart';
|
import 'fade_in_local_image.dart';
|
||||||
@@ -270,9 +270,9 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
|||||||
Icons.menu_book_outlined, '阅读', const Color(0xFF16A34A),
|
Icons.menu_book_outlined, '阅读', const Color(0xFF16A34A),
|
||||||
() { if (!widget.embedded) { Navigator.pop(context); } Navigator.push(context, MaterialPageRoute(builder: (_) => const BookFormPage())); },
|
() { if (!widget.embedded) { Navigator.pop(context); } Navigator.push(context, MaterialPageRoute(builder: (_) => const BookFormPage())); },
|
||||||
));
|
));
|
||||||
if (userPrefs.showNoteTab) actions.add((
|
if (userPrefs.showGameTab) actions.add((
|
||||||
Icons.edit_note_outlined, '笔记', const Color(0xFF9333EA),
|
Icons.sports_esports_outlined, '游戏', const Color(0xFFEA580C),
|
||||||
() { if (!widget.embedded) { Navigator.pop(context); } Navigator.push(context, MaterialPageRoute(builder: (_) => const NoteFormPage())); },
|
() { if (!widget.embedded) { Navigator.pop(context); } Navigator.push(context, MaterialPageRoute(builder: (_) => const GameFormPage())); },
|
||||||
));
|
));
|
||||||
|
|
||||||
if (actions.isEmpty) return const SizedBox.shrink();
|
if (actions.isEmpty) return const SizedBox.shrink();
|
||||||
|
|||||||
@@ -129,6 +129,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
version: "2.0.8"
|
||||||
|
excel:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: excel
|
||||||
|
sha256: "1a15327dcad260d5db21d1f6e04f04838109b39a2f6a84ea486ceda36e468780"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.6"
|
||||||
expandable:
|
expandable:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ dependencies:
|
|||||||
share_plus: ^10.0.0
|
share_plus: ^10.0.0
|
||||||
cross_file: ^0.3.4
|
cross_file: ^0.3.4
|
||||||
archive: ^3.6.0
|
archive: ^3.6.0
|
||||||
|
excel: ^4.0.6
|
||||||
xml: ^6.6.1
|
xml: ^6.6.1
|
||||||
permission_handler: ^11.3.0
|
permission_handler: ^11.3.0
|
||||||
flutter_staggered_grid_view: ^0.7.0
|
flutter_staggered_grid_view: ^0.7.0
|
||||||
|
|||||||
Reference in New Issue
Block a user