From 3557c38db7735fc1c7a3b6aa207fbdc22fa6472a Mon Sep 17 00:00:00 2001 From: ngfchl Date: Sat, 18 Jul 2026 13:43:22 +0800 Subject: [PATCH] feat: lazily load sqlite media artwork --- lib/core/storage/media_library_store.dart | 34 +++++++++++++++++++++++ lib/pages/media_library_page.dart | 31 ++++++++++++++------- lib/providers/media_library_provider.dart | 13 +++++++++ 3 files changed, 68 insertions(+), 10 deletions(-) diff --git a/lib/core/storage/media_library_store.dart b/lib/core/storage/media_library_store.dart index b089b4a..9429469 100644 --- a/lib/core/storage/media_library_store.dart +++ b/lib/core/storage/media_library_store.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:path/path.dart' as path; import 'package:sqflite/sqflite.dart'; @@ -95,6 +96,7 @@ class MediaLibraryStore { Future> items({String? libraryID}) async { final rows = await (await _db).query( 'media_items', + columns: _itemMetadataColumns, where: libraryID == null ? null : 'library_id = ?', whereArgs: libraryID == null ? null : [libraryID], orderBy: 'title COLLATE NOCASE', @@ -102,6 +104,18 @@ class MediaLibraryStore { return rows.map(_itemFromRow).toList(); } + Future posterBytes(String libraryID, String fileID) async { + final rows = await (await _db).query( + 'media_items', + columns: const ['poster'], + where: 'library_id = ? AND file_id = ?', + whereArgs: [libraryID, fileID], + limit: 1, + ); + final value = rows.isEmpty ? null : rows.first['poster']; + return value is Uint8List ? value : null; + } + Future saveLibraries(List libraries) async { final db = await _db; await db.transaction((txn) async { @@ -467,5 +481,25 @@ class MediaLibraryStore { } static const _rootFolderID = '@root'; + static const _itemMetadataColumns = [ + 'library_id', + 'file_id', + 'resource_path', + 'cloud_name', + 'file_size', + 'gcid', + 'file_type', + 'tmdb_id', + 'media_kind', + 'title', + 'original_title', + 'release_date', + 'overview', + 'has_chinese_audio', + 'has_chinese_subtitle', + 'collection_id', + 'collection_name', + 'updated_at', + ]; static String _folderID(String? folderID) => folderID ?? _rootFolderID; } diff --git a/lib/pages/media_library_page.dart b/lib/pages/media_library_page.dart index 87773dc..63e2c90 100644 --- a/lib/pages/media_library_page.dart +++ b/lib/pages/media_library_page.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:file_picker/file_picker.dart'; @@ -1221,13 +1223,13 @@ class _LibraryRow extends StatelessWidget { } } -class _MediaItemTile extends StatelessWidget { +class _MediaItemTile extends ConsumerWidget { final MediaLibraryItem item; const _MediaItemTile({required this.item}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final cs = ShadTheme.of(context).colorScheme; final posterURL = item.posterPath == null || item.posterPath!.isEmpty ? null @@ -1250,13 +1252,14 @@ class _MediaItemTile extends StatelessWidget { borderRadius: BorderRadius.circular(6), ), clipBehavior: Clip.antiAlias, - child: posterURL == null - ? Icon( - isSeries ? Icons.tv_rounded : Icons.movie_rounded, - size: 24, - color: cs.mutedForeground, - ) - : Image.network( + child: FutureBuilder( + future: ref.read(mediaLibraryProvider.notifier).posterBytes(item), + builder: (context, snapshot) { + if (snapshot.data != null) { + return Image.memory(snapshot.data!, fit: BoxFit.cover); + } + if (posterURL != null) { + return Image.network( posterURL, fit: BoxFit.cover, errorBuilder: (_, _, _) => Icon( @@ -1264,7 +1267,15 @@ class _MediaItemTile extends StatelessWidget { size: 24, color: cs.mutedForeground, ), - ), + ); + } + return Icon( + isSeries ? Icons.tv_rounded : Icons.movie_rounded, + size: 24, + color: cs.mutedForeground, + ); + }, + ), ), const SizedBox(width: 10), Expanded( diff --git a/lib/providers/media_library_provider.dart b/lib/providers/media_library_provider.dart index 311f543..54a68ac 100644 --- a/lib/providers/media_library_provider.dart +++ b/lib/providers/media_library_provider.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter_riverpod/legacy.dart'; @@ -85,6 +86,7 @@ class MediaLibraryState { class MediaLibraryNotifier extends StateNotifier { GuangyaAPI? _api; final _store = MediaLibraryStore(); + final _posterRequests = >{}; bool _loaded = false; bool _cancelScan = false; @@ -385,6 +387,17 @@ class MediaLibraryNotifier extends StateNotifier { ..sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase())); } + Future posterBytes(MediaLibraryItem item) { + final key = '${item.libraryID}:${item.file.id}'; + return _posterRequests.putIfAbsent(key, () async { + final bytes = await _store.posterBytes(item.libraryID, item.file.id); + if (_posterRequests.length > 48) { + _posterRequests.remove(_posterRequests.keys.first); + } + return bytes; + }); + } + void clearError() { state = state.copyWith(clearError: true); }