From 24fb7aab55b0cab4a21ae21dcd2fe2583bfd5cc2 Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Thu, 2 Jul 2026 19:49:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 - lib/pages/book/book_reviews_page.dart | 10 +- lib/pages/book/book_tab_page.dart | 22 +- lib/pages/epub_reader/epub_detail_page.dart | 27 +- lib/pages/main_content_page.dart | 26 +- lib/pages/movies/movie_posters_page.dart | 12 +- lib/pages/movies/movie_reviews_page.dart | 10 +- lib/pages/movies/movie_tab_page.dart | 24 +- lib/pages/note/note_form_page.dart | 16 +- lib/pages/profile_page.dart | 407 +++++++++++++++----- lib/providers/app_provider.dart | 24 +- lib/utils/user_prefs.dart | 8 + lib/widgets/tag_side_panel.dart | 64 +-- 13 files changed, 475 insertions(+), 177 deletions(-) diff --git a/README.md b/README.md index 7706237..acd4e61 100644 --- a/README.md +++ b/README.md @@ -209,8 +209,6 @@ server/ # Python Flask 后端 └── static/ # 静态资源 ``` -*共 134 个 Dart 源文件,8 个 Python 源文件。* - ## 数据存储 - **数据库位置**:`<应用文档目录>/mooknote.db` diff --git a/lib/pages/book/book_reviews_page.dart b/lib/pages/book/book_reviews_page.dart index b574187..9e4483e 100644 --- a/lib/pages/book/book_reviews_page.dart +++ b/lib/pages/book/book_reviews_page.dart @@ -96,14 +96,14 @@ class _BookReviewsPageState extends State { icon: Icon(_isSearching ? Icons.close : Icons.search), onPressed: _toggleSearch, ), - // 添加按钮 - IconButton( - icon: const Icon(Icons.add), - onPressed: () => _navigateToAddReview(), - ), const SizedBox(width: 8), ], ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () => _navigateToAddReview(), + icon: const Icon(Icons.add, size: 20), + label: const Text('添加书评'), + ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _filteredReviews.isEmpty diff --git a/lib/pages/book/book_tab_page.dart b/lib/pages/book/book_tab_page.dart index b370642..d2ace0b 100644 --- a/lib/pages/book/book_tab_page.dart +++ b/lib/pages/book/book_tab_page.dart @@ -99,12 +99,15 @@ class _BookTabPageState extends State { Future _loadFirst() async { final provider = context.read(); + final isWallMode = provider.bookshelfMode; final statusIdx = provider.bookStatusIndex; _lastStatusIndex = statusIdx; _initialized = true; - final status = _statusMap[statusIdx] ?? 'read'; + // 书架模式:不筛选状态,使用用户选择的排序(默认创建时间) + final status = isWallMode ? null : (_statusMap[statusIdx] ?? 'read'); + final sortMode = UserPrefs().bookSortMode; setState(() { _isLoading = true; _offset = 0; _hasMore = true; }); - final list = await provider.loadBooksPaged(status: status, offset: 0, sortMode: UserPrefs().bookSortMode); + final list = await provider.loadBooksPaged(status: status, offset: 0, sortMode: sortMode); if (!mounted) return; setState(() { _items.clear(); _items.addAll(list); _offset = list.length; _hasMore = list.length >= 20; _isLoading = false; }); } @@ -113,8 +116,10 @@ class _BookTabPageState extends State { if (_isLoading || !_hasMore) return; setState(() => _isLoading = true); final provider = context.read(); - final status = _statusMap[provider.bookStatusIndex] ?? 'read'; - final list = await provider.loadBooksPaged(status: status, offset: _offset, sortMode: UserPrefs().bookSortMode); + final isWallMode = provider.bookshelfMode; + final status = isWallMode ? null : (_statusMap[provider.bookStatusIndex] ?? 'read'); + final sortMode = UserPrefs().bookSortMode; + final list = await provider.loadBooksPaged(status: status, offset: _offset, sortMode: sortMode); if (!mounted) return; setState(() { _items.addAll(list); _offset += list.length; _hasMore = list.length >= 20; _isLoading = false; }); } @@ -128,8 +133,9 @@ class _BookTabPageState extends State { Widget build(BuildContext context) { final isWideContent = Breakpoint.isWideContent(context); final provider = context.watch(); + final isWallMode = provider.bookshelfMode; final masterContent = Column(children: [ - const BookStatusBar(), + if (!isWallMode) const BookStatusBar(), Expanded(child: _buildBody(context)), ]); @@ -289,13 +295,15 @@ class _BookTabPageState extends State { Widget _buildEmptyState(BuildContext context, int statusIndex) { final colors = Theme.of(context).colorScheme; - final statusText = ['已读', '在读', '想读'][statusIndex]; + final provider = context.read(); + final isWallMode = provider.bookshelfMode; + final statusText = isWallMode ? '' : ['已读', '在读', '想读'][statusIndex]; return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Container(width: 80, height: 80, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)), child: Icon(Icons.menu_book_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25))), const SizedBox(height: 20), - Text('暂无$statusText的书籍', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))), + Text(isWallMode ? '暂无书籍' : '暂无$statusText的书籍', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))), ])); } } diff --git a/lib/pages/epub_reader/epub_detail_page.dart b/lib/pages/epub_reader/epub_detail_page.dart index 79dae09..2fe7b1d 100644 --- a/lib/pages/epub_reader/epub_detail_page.dart +++ b/lib/pages/epub_reader/epub_detail_page.dart @@ -442,19 +442,22 @@ class _EpubDetailPageState extends State { Row( children: [ Icon(Icons.menu_book_outlined, size: 14, color: colors.primary.withValues(alpha: 0.6)), - const Spacer(), + const SizedBox(width: 6), if (excerpt.chapter.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: colors.primary.withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - excerpt.chapter, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 9, color: colors.primary.withValues(alpha: 0.7), fontWeight: FontWeight.w500), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: colors.primary.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + excerpt.chapter, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: TextStyle(fontSize: 9, color: colors.primary.withValues(alpha: 0.7), fontWeight: FontWeight.w500), + ), ), ), ], diff --git a/lib/pages/main_content_page.dart b/lib/pages/main_content_page.dart index be3e68f..80cca2a 100644 --- a/lib/pages/main_content_page.dart +++ b/lib/pages/main_content_page.dart @@ -296,17 +296,23 @@ class _MainContentPageState extends State { Future.delayed(const Duration(milliseconds: 400), () => _isTabTap = false); }, onLongPress: tab.label == '影视' - ? () => _showSortMenu(context, '影视排序', UserPrefs().movieSortMode, [ - (0, '按更新时间排序', Icons.update), - (1, '按创建时间排序', Icons.calendar_today_outlined), - (2, '按评分排序', Icons.star_outline), - ], (v) { UserPrefs().setMovieSortMode(v); context.read().loadMovies(); }) + ? () { + final isWallMode = UserPrefs().movieWallMode; + _showSortMenu(context, isWallMode ? '影视墙排序' : '影视排序', UserPrefs().movieSortMode, [ + (0, '按更新时间排序', Icons.update), + (1, '按创建时间排序', Icons.calendar_today_outlined), + (2, '按评分排序', Icons.star_outline), + ], (v) { UserPrefs().setMovieSortMode(v); context.read().loadMovies(); }); + } : tab.label == '阅读' - ? () => _showSortMenu(context, '书籍排序', UserPrefs().bookSortMode, [ - (0, '按更新时间排序', Icons.update), - (1, '按创建时间排序', Icons.calendar_today_outlined), - (2, '按评分排序', Icons.star_outline), - ], (v) { UserPrefs().setBookSortMode(v); context.read().loadBooks(); }) + ? () { + final isWallMode = UserPrefs().bookshelfMode; + _showSortMenu(context, isWallMode ? '书架排序' : '书籍排序', UserPrefs().bookSortMode, [ + (0, '按更新时间排序', Icons.update), + (1, '按创建时间排序', Icons.calendar_today_outlined), + (2, '按评分排序', Icons.star_outline), + ], (v) { UserPrefs().setBookSortMode(v); context.read().loadBooks(); }); + } : tab.label == '笔记' ? () => _showSortMenu(context, '笔记排序', UserPrefs().noteSortMode, [ (0, '按更新时间排序', Icons.update), diff --git a/lib/pages/movies/movie_posters_page.dart b/lib/pages/movies/movie_posters_page.dart index 9e88bde..0868fae 100644 --- a/lib/pages/movies/movie_posters_page.dart +++ b/lib/pages/movies/movie_posters_page.dart @@ -49,13 +49,11 @@ class _MoviePostersPageState extends State { backgroundColor: colors.surface, appBar: AppBar( title: const Text('海报墙'), - actions: [ - IconButton( - icon: const Icon(Icons.add_photo_alternate), - onPressed: _pickPoster, - ), - const SizedBox(width: 8), - ], + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: _pickPoster, + icon: const Icon(Icons.add_photo_alternate, size: 20), + label: const Text('添加海报'), ), body: _isLoading ? const Center(child: CircularProgressIndicator()) diff --git a/lib/pages/movies/movie_reviews_page.dart b/lib/pages/movies/movie_reviews_page.dart index cf1cb0d..52c8f4c 100644 --- a/lib/pages/movies/movie_reviews_page.dart +++ b/lib/pages/movies/movie_reviews_page.dart @@ -96,14 +96,14 @@ class _MovieReviewsPageState extends State { icon: Icon(_isSearching ? Icons.close : Icons.search), onPressed: _toggleSearch, ), - // 添加按钮 - IconButton( - icon: const Icon(Icons.add), - onPressed: () => _navigateToAddReview(), - ), const SizedBox(width: 8), ], ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () => _navigateToAddReview(), + icon: const Icon(Icons.add, size: 20), + label: const Text('添加影评'), + ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _filteredReviews.isEmpty diff --git a/lib/pages/movies/movie_tab_page.dart b/lib/pages/movies/movie_tab_page.dart index b2f05c6..6baaf03 100644 --- a/lib/pages/movies/movie_tab_page.dart +++ b/lib/pages/movies/movie_tab_page.dart @@ -92,12 +92,15 @@ class _MovieTabPageState extends State { Future _loadFirst() async { final provider = context.read(); + final isWallMode = provider.movieWallMode; final statusIdx = provider.movieStatusIndex; _lastStatusIndex = statusIdx; _initialized = true; - final status = _statusMap[statusIdx] ?? 'watched'; + // 影视墙模式:不筛选状态,使用用户选择的排序(默认创建时间) + final status = isWallMode ? null : (_statusMap[statusIdx] ?? 'watched'); + final sortMode = UserPrefs().movieSortMode; setState(() { _isLoading = true; _offset = 0; _hasMore = true; }); - final list = await provider.loadMoviesPaged(status: status, offset: 0, sortMode: UserPrefs().movieSortMode); + final list = await provider.loadMoviesPaged(status: status, offset: 0, sortMode: sortMode); if (!mounted) return; setState(() { _items.clear(); @@ -112,8 +115,10 @@ class _MovieTabPageState extends State { if (_isLoading || !_hasMore) return; setState(() => _isLoading = true); final provider = context.read(); - final status = _statusMap[provider.movieStatusIndex] ?? 'watched'; - final list = await provider.loadMoviesPaged(status: status, offset: _offset, sortMode: UserPrefs().movieSortMode); + final isWallMode = provider.movieWallMode; + final status = isWallMode ? null : (_statusMap[provider.movieStatusIndex] ?? 'watched'); + final sortMode = UserPrefs().movieSortMode; + final list = await provider.loadMoviesPaged(status: status, offset: _offset, sortMode: sortMode); if (!mounted) return; setState(() { _items.addAll(list); @@ -142,11 +147,12 @@ class _MovieTabPageState extends State { final colors = Theme.of(context).colorScheme; final isWideContent = Breakpoint.isWideContent(context); final provider = context.watch(); + final isWallMode = provider.movieWallMode; final masterContent = Column( children: [ - const MovieStatusBar(), - Divider(height: 0.5, thickness: 0.5, color: colors.outlineVariant), + if (!isWallMode) const MovieStatusBar(), + if (!isWallMode) Divider(height: 0.5, thickness: 0.5, color: colors.outlineVariant), Expanded(child: _buildBody(context)), ], ); @@ -470,13 +476,15 @@ class _MovieTabPageState extends State { Widget _buildEmptyState(BuildContext context, int statusIndex) { final colors = Theme.of(context).colorScheme; - final statusText = ['已看', '在看', '想看'][statusIndex]; + final provider = context.read(); + final isWallMode = provider.movieWallMode; + final statusText = isWallMode ? '' : ['已看', '在看', '想看'][statusIndex]; return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Container(width: 80, height: 80, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)), child: Icon(Icons.movie_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25))), const SizedBox(height: 20), - Text('暂无$statusText的影片', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))), + Text(isWallMode ? '暂无影片' : '暂无$statusText的影片', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))), ])); } } diff --git a/lib/pages/note/note_form_page.dart b/lib/pages/note/note_form_page.dart index 5cfd8a4..e99176b 100644 --- a/lib/pages/note/note_form_page.dart +++ b/lib/pages/note/note_form_page.dart @@ -679,8 +679,8 @@ class _NoteFormPageState extends State { try { final now = DateTime.now(); - if (_isEditing) { - // 更新现有笔记 + if (_isEditing && widget.note != null) { + // 更新现有笔记(编辑已有笔记) final updatedNote = widget.note!.copyWith( title: _titleController.text.trim(), content: content, @@ -689,8 +689,18 @@ class _NoteFormPageState extends State { updatedAt: now, ); await context.read().updateNote(updatedNote); + } else if (_savedNote != null) { + // 自动保存过的新笔记,更新它 + final updatedNote = _savedNote!.copyWith( + title: _titleController.text.trim(), + content: content, + tags: _tags, + images: _images, + updatedAt: now, + ); + await context.read().updateNote(updatedNote); } else { - // 添加新笔记 - 先创建笔记获取ID + // 添加新笔记 final noteId = now.millisecondsSinceEpoch.toString(); // 如果有图片,需要移动到正确的ID目录 diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart index ff1ab5b..9c8e53c 100644 --- a/lib/pages/profile_page.dart +++ b/lib/pages/profile_page.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; @@ -667,6 +668,12 @@ class _ProfilePageState extends State with RouteAware { context, MaterialPageRoute(builder: (_) => const StatisticsPage())) ), (Icons.backup_outlined, '备份', () => _showBackupOptions(context)), + ( + Icons.delete_outline, + '回收', + () => Navigator.push( + context, MaterialPageRoute(builder: (_) => const RecycleBinPage())) + ), ( Icons.settings_outlined, '设置', @@ -674,10 +681,9 @@ class _ProfilePageState extends State with RouteAware { context, MaterialPageRoute(builder: (_) => const SettingsPage())) ), ( - Icons.delete_outline, - '回收', - () => Navigator.push( - context, MaterialPageRoute(builder: (_) => const RecycleBinPage())) + Icons.feedback_outlined, + '反馈', + () => _showFeedbackDialog(context) ), ]; @@ -753,6 +759,96 @@ class _ProfilePageState extends State with RouteAware { return count.toString(); } + // ─── 反馈弹窗 ──────────────────────────────────────────────────────── + + void _showFeedbackDialog(BuildContext context) { + final colors = Theme.of(context).colorScheme; + final email = 'dellevin99@gmail.com'; + showModalBottomSheet( + context: context, + backgroundColor: colors.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + builder: (ctx) => SafeArea( + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Container( + width: 36, + height: 4, + margin: const EdgeInsets.only(top: 12, bottom: 16), + decoration: BoxDecoration( + color: colors.onSurface.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(2))), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Align( + alignment: Alignment.centerLeft, + child: Text('反馈', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colors.onSurface)))), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colors.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon(Icons.email_outlined, + size: 20, color: colors.primary.withValues(alpha: 0.8)), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('作者邮箱', + style: TextStyle( + fontSize: 12, + color: colors.onSurface.withValues(alpha: 0.5))), + const SizedBox(height: 2), + Text(email, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colors.onSurface)), + ], + ), + ), + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: email)); + ToastUtil.show(context, '已复制到剪贴板'); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: colors.primary.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.copy, size: 14, color: colors.primary), + const SizedBox(width: 4), + Text('复制', style: TextStyle(fontSize: 12, color: colors.primary, fontWeight: FontWeight.w600)), + ], + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + ]), + ), + ); + } + // ─── 头像 ──────────────────────────────────────────────────────────── Future _pickAvatar() async { @@ -990,7 +1086,7 @@ class _SettingsPageState extends State { height: 0.5, indent: 24, endIndent: 24, - color: colors.outlineVariant), + color: colors.outlineVariant), _buildNavigationItem( icon: Icons.tune_outlined, title: '功能设置', @@ -2596,6 +2692,8 @@ class _LayoutSettingsPageState extends State { int _noteLayout = 0; int _movieLayout = 0; int _bookLayout = 0; + bool _movieWallMode = false; + bool _bookshelfMode = false; @override void initState() { @@ -2603,6 +2701,8 @@ class _LayoutSettingsPageState extends State { _noteLayout = _userPrefs.noteLayoutStyle; _movieLayout = _userPrefs.movieLayoutStyle; _bookLayout = _userPrefs.bookLayoutStyle; + _movieWallMode = _userPrefs.movieWallMode; + _bookshelfMode = _userPrefs.bookshelfMode; } @override @@ -2612,63 +2712,168 @@ class _LayoutSettingsPageState extends State { backgroundColor: colors.surface, appBar: AppBar(title: const Text('布局设置')), body: ListView( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 16), children: [ - _buildSection( - '影视布局', - [ - ButtonSegment( - value: 0, - icon: Icon(Icons.grid_view_outlined, size: 16), - label: Text('海报网格', style: TextStyle(fontSize: 12))), - ButtonSegment( - value: 1, - icon: Icon(Icons.view_list_outlined, size: 16), - label: Text('列表', style: TextStyle(fontSize: 12))), - ButtonSegment( - value: 2, - icon: Icon(Icons.crop_landscape_outlined, size: 16), - label: Text('大图卡片', style: TextStyle(fontSize: 12))), - ], - _movieLayout, - (v) => _setLayout('movie', v)), - _buildSection( - '阅读布局', - [ - ButtonSegment( - value: 0, - icon: Icon(Icons.grid_view_outlined, size: 16), - label: Text('封面网格', style: TextStyle(fontSize: 12))), - ButtonSegment( - value: 1, - icon: Icon(Icons.view_list_outlined, size: 16), - label: Text('列表', style: TextStyle(fontSize: 12))), - ], - _bookLayout, - (v) => _setLayout('book', v)), - _buildSection( - '笔记布局', - [ - ButtonSegment( - value: 0, - icon: Icon(Icons.view_list_outlined, size: 16), - label: Text('列表', style: TextStyle(fontSize: 12))), - ButtonSegment( - value: 1, - icon: Icon(Icons.grid_view_outlined, size: 16), - label: Text('瀑布流', style: TextStyle(fontSize: 12))), - ButtonSegment( - value: 2, - icon: Icon(Icons.timeline_outlined, size: 16), - label: Text('时间线', style: TextStyle(fontSize: 12))), - ], - _noteLayout, - (v) => _setLayout('note', v)), + // ── 影视 ── + _buildCategoryHeader(Icons.movie_outlined, '影视', colors.primary), + _buildWallSwitch( + icon: Icons.wallpaper_outlined, + title: '影视墙模式', + subtitle: '显示全部影片,不区分状态', + value: _movieWallMode, + onChanged: (v) => _setWallMode('movie', v), + ), + _buildLayoutSelector( + selected: _movieLayout, + options: [ + (0, Icons.grid_view_outlined, '海报网格'), + (1, Icons.view_list_outlined, '列表'), + (2, Icons.crop_landscape_outlined, '大图卡片'), + ], + onChanged: (v) => _setLayout('movie', v), + ), + const SizedBox(height: 8), + Divider( + height: 1, + indent: 16, + endIndent: 16, + color: colors.outlineVariant.withValues(alpha: 0.5)), + const SizedBox(height: 8), + + // ── 阅读 ── + _buildCategoryHeader(Icons.menu_book_outlined, '阅读', colors.primary), + _buildWallSwitch( + icon: Icons.auto_stories_outlined, + title: '书架模式', + subtitle: '显示全部书籍,不区分状态', + value: _bookshelfMode, + onChanged: (v) => _setWallMode('book', v), + ), + _buildLayoutSelector( + selected: _bookLayout, + options: [ + (0, Icons.grid_view_outlined, '海报网格'), + (1, Icons.view_list_outlined, '列表'), + ], + onChanged: (v) => _setLayout('book', v), + ), + const SizedBox(height: 8), + Divider( + height: 1, + indent: 16, + endIndent: 16, + color: colors.outlineVariant.withValues(alpha: 0.5)), + const SizedBox(height: 8), + + // ── 笔记 ── + _buildCategoryHeader(Icons.note_outlined, '笔记', colors.primary), + _buildLayoutSelector( + selected: _noteLayout, + options: [ + (0, Icons.view_list_outlined, '列表'), + (1, Icons.grid_view_outlined, '瀑布流'), + (2, Icons.timeline_outlined, '时间线'), + ], + onChanged: (v) => _setLayout('note', v), + ), ], ), ); } + Widget _buildCategoryHeader(IconData icon, String title, Color color) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), + child: Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, size: 16, color: color), + ), + const SizedBox(width: 10), + Text(title, + style: TextStyle( + fontSize: 15, fontWeight: FontWeight.w700, color: color)), + ], + ), + ); + } + + Widget _buildWallSwitch({ + required IconData icon, + required String title, + required String subtitle, + required bool value, + required ValueChanged onChanged, + }) { + final colors = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: colors.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colors.primary.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, + size: 20, color: colors.primary.withValues(alpha: 0.8)), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colors.onSurface)), + const SizedBox(height: 2), + Text(subtitle, + style: TextStyle( + fontSize: 12, + color: colors.onSurface.withValues(alpha: 0.5))), + ], + ), + ), + Switch( + value: value, + onChanged: onChanged, + activeThumbColor: colors.primary, + ), + ], + ), + ), + ); + } + + void _setWallMode(String type, bool value) async { + switch (type) { + case 'movie': + await _userPrefs.setMovieWallMode(value); + setState(() => _movieWallMode = value); + if (mounted) context.read().setMovieWallMode(value); + case 'book': + await _userPrefs.setBookshelfMode(value); + setState(() => _bookshelfMode = value); + if (mounted) context.read().setBookshelfMode(value); + } + } + void _setLayout(String type, int value) async { switch (type) { case 'note': @@ -2684,46 +2889,64 @@ class _LayoutSettingsPageState extends State { } } - Widget _buildSection(String title, List> segments, - int selected, ValueChanged onChanged) { + Widget _buildLayoutSelector({ + required int selected, + required List<(int, IconData, String)> options, + required ValueChanged onChanged, + }) { final colors = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 4, bottom: 8), - child: Text(title, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: colors.onSurface)), - ), - SegmentedButton( - segments: segments, - selected: {selected}, - onSelectionChanged: (s) => onChanged(s.first), - showSelectedIcon: false, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith((s) => - s.contains(WidgetState.selected) - ? colors.onSurface - : colors.surfaceContainerHighest), - foregroundColor: WidgetStateProperty.resolveWith((s) => - s.contains(WidgetState.selected) - ? colors.surface - : colors.onSurface.withValues(alpha: 0.6)), - side: WidgetStateProperty.all(BorderSide.none), - shape: WidgetStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10))), - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric(vertical: 10)), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.standard, + child: Row( + children: options.map((opt) { + final isSelected = selected == opt.$1; + return Expanded( + child: GestureDetector( + onTap: () => onChanged(opt.$1), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: isSelected + ? colors.primary + : colors.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? colors.primary + : colors.outlineVariant.withValues(alpha: 0.5), + width: isSelected ? 0 : 0.5, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + opt.$2, + size: 22, + color: isSelected + ? colors.onPrimary + : colors.onSurface.withValues(alpha: 0.5), + ), + const SizedBox(height: 6), + Text( + opt.$3, + style: TextStyle( + fontSize: 12, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w500, + color: isSelected + ? colors.onPrimary + : colors.onSurface.withValues(alpha: 0.5), + ), + ), + ], + ), + ), ), - ), - ], + ); + }).toList(), ), ); } diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index d15c6bd..16de901 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -61,9 +61,15 @@ class AppProvider extends ChangeNotifier { // 影视列表布局样式 (0: 网格, 1: 列表, 2: 大图卡片) int _movieLayoutStyle = 0; - + + // 影视墙模式(不显示分类,按创建时间排序) + bool _movieWallMode = false; + // 阅读选中的状态 (0: 读完,1: 在读,2: 准备读) int _bookStatusIndex = 0; + + // 书架模式(不显示分类,按创建时间排序) + bool _bookshelfMode = false; // 侧边菜单是否打开 bool _drawerOpen = false; @@ -95,6 +101,8 @@ class AppProvider extends ChangeNotifier { void initMainTabIndex() { final userPrefs = UserPrefs(); _movieLayoutStyle = userPrefs.movieLayoutStyle; + _movieWallMode = userPrefs.movieWallMode; + _bookshelfMode = userPrefs.bookshelfMode; final defaultIndex = userPrefs.defaultMainTabIndex; // 确保选中的标签是启用的 final showMovie = userPrefs.showMovieTab; @@ -164,7 +172,9 @@ class AppProvider extends ChangeNotifier { Note? get selectedNote => _selectedNote; int get movieStatusIndex => _movieStatusIndex; int get movieLayoutStyle => _movieLayoutStyle; + bool get movieWallMode => _movieWallMode; int get bookStatusIndex => _bookStatusIndex; + bool get bookshelfMode => _bookshelfMode; bool get drawerOpen => _drawerOpen; bool get bottomNavVisible => _bottomNavVisible; ThemeMode get themeMode => _themeMode; @@ -276,11 +286,23 @@ class AppProvider extends ChangeNotifier { notifyListeners(); } + void setMovieWallMode(bool enabled) { + _movieWallMode = enabled; + UserPrefs().setMovieWallMode(enabled); + notifyListeners(); + } + void setBookStatusIndex(int index) { _bookStatusIndex = index; notifyListeners(); } + void setBookshelfMode(bool enabled) { + _bookshelfMode = enabled; + UserPrefs().setBookshelfMode(enabled); + notifyListeners(); + } + void toggleDrawer() { _drawerOpen = !_drawerOpen; notifyListeners(); diff --git a/lib/utils/user_prefs.dart b/lib/utils/user_prefs.dart index 3bde8ba..84c6009 100644 --- a/lib/utils/user_prefs.dart +++ b/lib/utils/user_prefs.dart @@ -165,6 +165,14 @@ class UserPrefs { int get bookLayoutStyle => prefs.getInt('bookLayoutStyle') ?? 0; Future setBookLayoutStyle(int value) => prefs.setInt('bookLayoutStyle', value); + /// 影视墙模式(不显示分类,按创建时间排序) + bool get movieWallMode => prefs.getBool('movieWallMode') ?? false; + Future setMovieWallMode(bool value) => prefs.setBool('movieWallMode', value); + + /// 书架模式(不显示分类,按创建时间排序) + bool get bookshelfMode => prefs.getBool('bookshelfMode') ?? false; + Future setBookshelfMode(bool value) => prefs.setBool('bookshelfMode', value); + // ========== 应用图标设置 ========== // ========== Markdown 阅读器 ========== diff --git a/lib/widgets/tag_side_panel.dart b/lib/widgets/tag_side_panel.dart index a50417b..2a73cde 100644 --- a/lib/widgets/tag_side_panel.dart +++ b/lib/widgets/tag_side_panel.dart @@ -107,9 +107,16 @@ class _TagSidePanelState extends State { children: [ Text('标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), const Spacer(), - IconButton( - onPressed: () => Navigator.pop(context), - icon: Icon(Icons.close, size: 20, color: colors.onSurface.withValues(alpha: 0.6)), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + decoration: BoxDecoration( + color: colors.primary, + borderRadius: BorderRadius.circular(20), + ), + child: Text('保存', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onPrimary)), + ), ), ], ), @@ -160,29 +167,34 @@ class _TagSidePanelState extends State { ), Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), - child: Wrap( - spacing: 8, - runSpacing: 8, - children: _selected.map((tag) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: colors.primary, - borderRadius: BorderRadius.circular(16), - ), - child: GestureDetector( - onTap: () => _toggleTag(tag), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(tag, style: TextStyle(fontSize: 13, color: colors.onPrimary)), - const SizedBox(width: 4), - Icon(Icons.close, size: 14, color: colors.onPrimary), - ], + child: SizedBox( + width: double.infinity, + child: Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + children: _selected.map((tag) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: colors.primary, + borderRadius: BorderRadius.circular(16), ), - ), - ); - }).toList(), + child: GestureDetector( + onTap: () => _toggleTag(tag), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(tag, style: TextStyle(fontSize: 13, color: colors.onPrimary)), + const SizedBox(width: 4), + Icon(Icons.close, size: 14, color: colors.onPrimary), + ], + ), + ), + ); + }).toList(), + ), ), ), ], @@ -201,6 +213,8 @@ class _TagSidePanelState extends State { child: Wrap( spacing: 8, runSpacing: 8, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, children: widget.allAvailableTags.map((tag) { final isSelected = _selected.contains(tag); return GestureDetector(