From b4793a8d0138599aaca134091dfeee8851a10c71 Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Mon, 13 Jul 2026 20:28:12 +0800 Subject: [PATCH] =?UTF-8?q?windows=E7=89=88=E6=9C=AC=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/book/book_dao.dart | 6 +- lib/data/game/game_dao.dart | 6 +- lib/data/movie/movie_dao.dart | 6 +- lib/data/note/note_dao.dart | 2 +- lib/pages/home/home_page.dart | 249 ++++++++++++++++++++++++++------ lib/providers/app_provider.dart | 18 +-- 6 files changed, 224 insertions(+), 63 deletions(-) diff --git a/lib/data/book/book_dao.dart b/lib/data/book/book_dao.dart index 37c16e6..68f3be0 100644 --- a/lib/data/book/book_dao.dart +++ b/lib/data/book/book_dao.dart @@ -16,13 +16,13 @@ class BookDao { } // 获取所有未删除的书籍记录 - Future> getAllBooks() => _wrap('getAllBooks', () async { + Future> getAllBooks({int sortMode = 0}) => _wrap('getAllBooks', () async { final db = await _dbHelper.database; final List> maps = await db.query( 'books', where: 'is_deleted = ?', whereArgs: [0], - orderBy: 'created_at DESC', + orderBy: _buildBookOrderBy(sortMode), ); return List.generate(maps.length, (i) => Book.fromJson(maps[i])); }); @@ -45,7 +45,7 @@ class BookDao { switch (sortMode) { case 1: return 'created_at DESC'; case 2: return 'rating DESC NULLS LAST, updated_at DESC'; - default: return 'created_at DESC'; + default: return 'updated_at DESC'; } } diff --git a/lib/data/game/game_dao.dart b/lib/data/game/game_dao.dart index 9321f0f..979f0c7 100644 --- a/lib/data/game/game_dao.dart +++ b/lib/data/game/game_dao.dart @@ -16,13 +16,13 @@ class GameDao { } // 获取所有游戏记录(未删除的) - Future> getAllGames() => _wrap('getAllGames', () async { + Future> getAllGames({int sortMode = 0}) => _wrap('getAllGames', () async { final db = await _dbHelper.database; final List> maps = await db.query( 'games', where: 'is_deleted = ?', whereArgs: [0], - orderBy: 'created_at DESC', + orderBy: _buildGameOrderBy(sortMode), ); return List.generate(maps.length, (i) => Game.fromJson(maps[i])); }); @@ -45,7 +45,7 @@ class GameDao { switch (sortMode) { case 1: return 'created_at DESC'; case 2: return 'rating DESC NULLS LAST, updated_at DESC'; - default: return 'created_at DESC'; + default: return 'updated_at DESC'; } } diff --git a/lib/data/movie/movie_dao.dart b/lib/data/movie/movie_dao.dart index 7a5756b..4c8d578 100644 --- a/lib/data/movie/movie_dao.dart +++ b/lib/data/movie/movie_dao.dart @@ -16,13 +16,13 @@ class MovieDao { } // 获取所有影视记录(未删除的) - Future> getAllMovies() => _wrap('getAllMovies', () async { + Future> getAllMovies({int sortMode = 0}) => _wrap('getAllMovies', () async { final db = await _dbHelper.database; final List> maps = await db.query( 'movies', where: 'is_deleted = ?', whereArgs: [0], - orderBy: 'created_at DESC', + orderBy: _buildMovieOrderBy(sortMode), ); return List.generate(maps.length, (i) => Movie.fromJson(maps[i])); }); @@ -49,7 +49,7 @@ class MovieDao { switch (sortMode) { case 1: return 'created_at DESC'; case 2: return 'rating DESC NULLS LAST, updated_at DESC'; - default: return 'created_at DESC'; + default: return 'updated_at DESC'; } } diff --git a/lib/data/note/note_dao.dart b/lib/data/note/note_dao.dart index 6210f43..8e598df 100644 --- a/lib/data/note/note_dao.dart +++ b/lib/data/note/note_dao.dart @@ -40,7 +40,7 @@ class NoteDao { switch (sortMode) { case 1: return 'is_pinned DESC, created_at DESC'; case 2: return 'is_pinned DESC, title COLLATE NOCASE ASC'; - default: return 'is_pinned DESC, created_at DESC'; + default: return 'is_pinned DESC, updated_at DESC'; } } diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 97c5e79..a4cf8ae 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -577,8 +577,8 @@ class _EncounterDialog extends StatelessWidget { contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0), title: const Text('统计'), content: SizedBox( - width: 400, - height: 340, + width: 360, + height: 440, child: Consumer( builder: (context, provider, child) { final movies = provider.movies.where((m) => !m.isDeleted).toList(); @@ -997,8 +997,8 @@ class _StrollDialogState extends State<_StrollDialog> { elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), child: SizedBox( - width: 580, - height: 520, + width: 520, + height: 620, child: Column( children: [ // 标题栏 @@ -1303,8 +1303,8 @@ class _CalendarDialogState extends State<_CalendarDialog> { icon: Icon(Icons.chevron_right, size: 20, color: colors.onSurface.withValues(alpha: 0.6)), padding: EdgeInsets.zero, constraints: const BoxConstraints()), ]), content: SizedBox( - width: 480, - height: 440, + width: 420, + height: 520, child: Column( children: [ // 星期头 @@ -3648,6 +3648,16 @@ class _DesktopListPanelState extends State<_DesktopListPanel> { final TextEditingController _searchCtrl = TextEditingController(); String _keyword = ''; Timer? _debounce; + /// 状态筛选:null=全部,否则按各实体 status 值过滤 + String? _statusFilter; + + @override + void didUpdateWidget(covariant _DesktopListPanel oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.mainTabIndex != widget.mainTabIndex) { + _statusFilter = null; + } + } @override void dispose() { @@ -3672,43 +3682,51 @@ class _DesktopListPanelState extends State<_DesktopListPanel> { SizedBox(height: MediaQuery.of(context).padding.top), Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 4), - child: SizedBox( - height: 36, - child: TextField( - controller: _searchCtrl, - onChanged: _onSearchChanged, - style: TextStyle(fontSize: 13, color: colors.onSurface), - decoration: InputDecoration( - hintText: '搜索...', - hintStyle: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.35)), - prefixIcon: Icon(Icons.search, size: 18, color: colors.onSurface.withValues(alpha: 0.4)), - suffixIcon: _keyword.isNotEmpty - ? GestureDetector( - onTap: () { - _searchCtrl.clear(); - setState(() => _keyword = ''); - }, - child: Icon(Icons.close, size: 16, color: colors.onSurface.withValues(alpha: 0.4)), - ) - : null, - isDense: true, - contentPadding: const EdgeInsets.symmetric(vertical: 0), - filled: true, - fillColor: colors.surfaceContainerHighest.withValues(alpha: 0.5), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: colors.primary.withValues(alpha: 0.3), width: 1), + child: Row( + children: [ + Expanded( + child: SizedBox( + height: 36, + child: TextField( + controller: _searchCtrl, + onChanged: _onSearchChanged, + style: TextStyle(fontSize: 13, color: colors.onSurface), + decoration: InputDecoration( + hintText: '搜索...', + hintStyle: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.35)), + prefixIcon: Icon(Icons.search, size: 18, color: colors.onSurface.withValues(alpha: 0.4)), + suffixIcon: _keyword.isNotEmpty + ? GestureDetector( + onTap: () { + _searchCtrl.clear(); + setState(() => _keyword = ''); + }, + child: Icon(Icons.close, size: 16, color: colors.onSurface.withValues(alpha: 0.4)), + ) + : null, + isDense: true, + contentPadding: const EdgeInsets.symmetric(vertical: 0), + filled: true, + fillColor: colors.surfaceContainerHighest.withValues(alpha: 0.5), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: colors.primary.withValues(alpha: 0.3), width: 1), + ), + ), + ), ), ), - ), + const SizedBox(width: 8), + _buildSortButton(context), + ], ), ), const SizedBox(height: 4), @@ -3734,10 +3752,153 @@ class _DesktopListPanelState extends State<_DesktopListPanel> { } } + // ─── 排序按钮(对应 Android 版长按 tab 弹出的排序菜单)──────────── + + Widget _buildSortButton(BuildContext context) { + final colors = Theme.of(context).colorScheme; + final (currentSort, sortOptions, _, onSortSelected) = _sortConfig(context); + final statusOptions = _statusFilterOptions(); + return PopupMenuButton( + tooltip: '排序与筛选', + position: PopupMenuPosition.under, + color: colors.surface, + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + itemBuilder: (ctx) => [ + // 排序区 + PopupMenuItem( + value: '__sort_header__', + enabled: false, + height: 28, + child: Text('排序', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.4))), + ), + for (final (value, text, icon) in sortOptions) + PopupMenuItem( + value: 'sort:$value', + child: Row(children: [ + Icon(icon, size: 16, color: currentSort == value ? colors.primary : colors.onSurface.withValues(alpha: 0.6)), + const SizedBox(width: 10), + Text(text, style: TextStyle(fontSize: 13, color: currentSort == value ? colors.primary : colors.onSurface)), + const Spacer(), + if (currentSort == value) + Icon(Icons.check, size: 14, color: colors.primary), + ]), + ), + // 状态筛选区(仅影视/阅读/游戏) + if (statusOptions != null) ...[ + const PopupMenuDivider(height: 8), + PopupMenuItem( + value: '__filter_header__', + enabled: false, + height: 28, + child: Text('状态筛选', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.4))), + ), + for (final (value, text, icon) in statusOptions) + PopupMenuItem( + value: 'filter:$value', + child: Row(children: [ + Icon(icon, size: 16, color: _statusFilter == value ? colors.primary : colors.onSurface.withValues(alpha: 0.6)), + const SizedBox(width: 10), + Text(text, style: TextStyle(fontSize: 13, color: _statusFilter == value ? colors.primary : colors.onSurface)), + const Spacer(), + if (_statusFilter == value) + Icon(Icons.check, size: 14, color: colors.primary), + ]), + ), + ], + ], + onSelected: (v) { + if (v.startsWith('sort:')) { + final sortVal = int.parse(v.substring(5)); + if (sortVal != currentSort) onSortSelected(sortVal); + } else if (v.startsWith('filter:')) { + final filterVal = v.substring(7); + setState(() => _statusFilter = filterVal.isEmpty ? null : filterVal); + } + }, + child: Container( + height: 36, + width: 36, + decoration: BoxDecoration( + color: colors.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(Icons.sort, size: 18, color: colors.onSurface.withValues(alpha: 0.6)), + ), + ); + } + + /// 各分类的状态筛选选项,笔记返回 null(无状态筛选) + List<(String, String, IconData)>? _statusFilterOptions() { + switch (widget.mainTabIndex) { + case 0: // 影视 + return [ + ('', '全部', Icons.select_all), + ('watched', '已看', Icons.visibility), + ('watching', '在看', Icons.play_circle_outline), + ('want_to_watch', '想看', Icons.bookmark_outline), + ]; + case 1: // 阅读 + return [ + ('', '全部', Icons.select_all), + ('read', '已读', Icons.visibility), + ('reading', '在读', Icons.play_circle_outline), + ('want_to_read', '想读', Icons.bookmark_outline), + ('abandoned', '弃读', Icons.block), + ]; + case 3: // 游戏 + return [ + ('', '全部', Icons.select_all), + ('completed', '通关', Icons.visibility), + ('playing', '在玩', Icons.play_circle_outline), + ('want_to_play', '想玩', Icons.bookmark_outline), + ('abandoned', '弃游', Icons.block), + ]; + default: + return null; + } + } + + (int, List<(int, String, IconData)>, String, ValueChanged) _sortConfig(BuildContext context) { + final provider = context.read(); + switch (widget.mainTabIndex) { + case 0: + return ( + UserPrefs().movieSortMode, + [(0, '按更新时间', Icons.update), (1, '按创建时间', Icons.calendar_today_outlined), (2, '按评分', Icons.star_outline)], + '影视排序', + (v) { UserPrefs().setMovieSortMode(v); provider.loadMovies(); }, + ); + case 1: + return ( + UserPrefs().bookSortMode, + [(0, '按更新时间', Icons.update), (1, '按创建时间', Icons.calendar_today_outlined), (2, '按评分', Icons.star_outline)], + '书籍排序', + (v) { UserPrefs().setBookSortMode(v); provider.loadBooks(); }, + ); + case 2: + return ( + UserPrefs().noteSortMode, + [(0, '按更新时间', Icons.update), (1, '按创建时间', Icons.calendar_today_outlined)], + '笔记排序', + (v) { UserPrefs().setNoteSortMode(v); provider.loadNotes(); }, + ); + case 3: + return ( + UserPrefs().gameSortMode, + [(0, '按更新时间', Icons.update), (1, '按创建时间', Icons.calendar_today_outlined), (2, '按评分', Icons.star_outline)], + '游戏排序', + (v) { UserPrefs().setGameSortMode(v); provider.loadGames(); }, + ); + default: + return (0, <(int, String, IconData)>[], '', (_) {}); + } + } + Widget _buildMovieList(BuildContext context) { return Consumer( builder: (context, provider, _) { - final items = provider.movies.where((m) => !m.isDeleted).toList(); + final items = provider.movies.where((m) => !m.isDeleted && (_statusFilter == null || m.status == _statusFilter)).toList(); if (items.isEmpty) return _buildEmpty('暂无影视记录', Icons.movie_outlined); return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 4), @@ -3765,7 +3926,7 @@ class _DesktopListPanelState extends State<_DesktopListPanel> { Widget _buildBookList(BuildContext context) { return Consumer( builder: (context, provider, _) { - final items = provider.books.where((b) => !b.isDeleted).toList(); + final items = provider.books.where((b) => !b.isDeleted && (_statusFilter == null || b.status == _statusFilter)).toList(); if (items.isEmpty) return _buildEmpty('暂无阅读记录', Icons.menu_book_outlined); return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 4), @@ -3817,7 +3978,7 @@ class _DesktopListPanelState extends State<_DesktopListPanel> { Widget _buildGameList(BuildContext context) { return Consumer( builder: (context, provider, _) { - final items = provider.games.where((g) => !g.isDeleted).toList(); + final items = provider.games.where((g) => !g.isDeleted && (_statusFilter == null || g.status == _statusFilter)).toList(); if (items.isEmpty) return _buildEmpty('暂无游戏记录', Icons.sports_esports_outlined); return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 4), diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 4f9f4e3..19a2b95 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -129,22 +129,22 @@ class AppProvider extends ChangeNotifier { debugPrint('[AppProvider] initDatabase'); // 独立加载每个 DAO,避免一个失败导致全部中断 try { - _movies = await _movieDao.getAllMovies(); + _movies = await _movieDao.getAllMovies(sortMode: UserPrefs().movieSortMode); } catch (e) { debugPrint('[AppProvider] 加载影视数据失败: $e'); } try { - _books = await _bookDao.getAllBooks(); + _books = await _bookDao.getAllBooks(sortMode: UserPrefs().bookSortMode); } catch (e) { debugPrint('[AppProvider] 加载书籍数据失败: $e'); } try { - _notes = await _noteDao.getAllNotes(); + _notes = await _noteDao.getAllNotes(sortMode: UserPrefs().noteSortMode); } catch (e) { debugPrint('[AppProvider] 加载笔记数据失败: $e'); } try { - _games = await _gameDao.getAllGames(); + _games = await _gameDao.getAllGames(sortMode: UserPrefs().gameSortMode); } catch (e) { debugPrint('[AppProvider] 加载游戏数据失败: $e'); } @@ -198,25 +198,25 @@ class AppProvider extends ChangeNotifier { // 加载影视数据 Future loadMovies() async { - _movies = await _movieDao.getAllMovies(); + _movies = await _movieDao.getAllMovies(sortMode: UserPrefs().movieSortMode); notifyListeners(); } // 加载书籍数据 Future loadBooks() async { - _books = await _bookDao.getAllBooks(); + _books = await _bookDao.getAllBooks(sortMode: UserPrefs().bookSortMode); notifyListeners(); } // 加载笔记数据 - Future loadNotes({int sortMode = 0}) async { - _notes = await _noteDao.getAllNotes(sortMode: sortMode); + Future loadNotes({int? sortMode}) async { + _notes = await _noteDao.getAllNotes(sortMode: sortMode ?? UserPrefs().noteSortMode); notifyListeners(); } // 加载游戏数据 Future loadGames() async { - _games = await _gameDao.getAllGames(); + _games = await _gameDao.getAllGames(sortMode: UserPrefs().gameSortMode); notifyListeners(); }