generated from dellevin/template
windows版本搜索功能优化
This commit is contained in:
@@ -16,13 +16,13 @@ class BookDao {
|
||||
}
|
||||
|
||||
// 获取所有未删除的书籍记录
|
||||
Future<List<Book>> getAllBooks() => _wrap('getAllBooks', () async {
|
||||
Future<List<Book>> getAllBooks({int sortMode = 0}) => _wrap('getAllBooks', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> 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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@ class GameDao {
|
||||
}
|
||||
|
||||
// 获取所有游戏记录(未删除的)
|
||||
Future<List<Game>> getAllGames() => _wrap('getAllGames', () async {
|
||||
Future<List<Game>> getAllGames({int sortMode = 0}) => _wrap('getAllGames', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> 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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@ class MovieDao {
|
||||
}
|
||||
|
||||
// 获取所有影视记录(未删除的)
|
||||
Future<List<Movie>> getAllMovies() => _wrap('getAllMovies', () async {
|
||||
Future<List<Movie>> getAllMovies({int sortMode = 0}) => _wrap('getAllMovies', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> 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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AppProvider>(
|
||||
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<String>(
|
||||
tooltip: '排序与筛选',
|
||||
position: PopupMenuPosition.under,
|
||||
color: colors.surface,
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
itemBuilder: (ctx) => [
|
||||
// 排序区
|
||||
PopupMenuItem<String>(
|
||||
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<String>(
|
||||
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<String>(
|
||||
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<String>(
|
||||
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<int>) _sortConfig(BuildContext context) {
|
||||
final provider = context.read<AppProvider>();
|
||||
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<AppProvider>(
|
||||
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<AppProvider>(
|
||||
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<AppProvider>(
|
||||
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),
|
||||
|
||||
@@ -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<void> loadMovies() async {
|
||||
_movies = await _movieDao.getAllMovies();
|
||||
_movies = await _movieDao.getAllMovies(sortMode: UserPrefs().movieSortMode);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 加载书籍数据
|
||||
Future<void> loadBooks() async {
|
||||
_books = await _bookDao.getAllBooks();
|
||||
_books = await _bookDao.getAllBooks(sortMode: UserPrefs().bookSortMode);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 加载笔记数据
|
||||
Future<void> loadNotes({int sortMode = 0}) async {
|
||||
_notes = await _noteDao.getAllNotes(sortMode: sortMode);
|
||||
Future<void> loadNotes({int? sortMode}) async {
|
||||
_notes = await _noteDao.getAllNotes(sortMode: sortMode ?? UserPrefs().noteSortMode);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 加载游戏数据
|
||||
Future<void> loadGames() async {
|
||||
_games = await _gameDao.getAllGames();
|
||||
_games = await _gameDao.getAllGames(sortMode: UserPrefs().gameSortMode);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user