feat: lazily load sqlite media artwork
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user