feat: lazily load sqlite media artwork

This commit is contained in:
ngfchl
2026-07-18 13:43:22 +08:00
parent c7ba13e7bf
commit 3557c38db7
3 changed files with 68 additions and 10 deletions
+34
View File
@@ -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<List<MediaLibraryItem>> 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<Uint8List?> 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<void> saveLibraries(List<MediaLibraryDefinition> 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;
}
+21 -10
View File
@@ -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<Uint8List?>(
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(
+13
View File
@@ -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<MediaLibraryState> {
GuangyaAPI? _api;
final _store = MediaLibraryStore();
final _posterRequests = <String, Future<Uint8List?>>{};
bool _loaded = false;
bool _cancelScan = false;
@@ -385,6 +387,17 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
..sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()));
}
Future<Uint8List?> 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);
}