#110 video: resume playback

This commit is contained in:
Thibault Deckers 2021-11-06 17:52:43 +09:00
parent c6c87bdc96
commit 87b007c60f
18 changed files with 321 additions and 40 deletions

View file

@ -341,6 +341,17 @@
}
},
"videoResumeDialogMessage": "Do you want to resume playing at {time}?",
"@videoResumeDialogMessage": {
"placeholders": {
"time": {}
}
},
"videoStartOverButtonLabel": "START OVER",
"@videoStartOverButtonLabel": {},
"videoResumeButtonLabel": "RESUME",
"@videoResumeButtonLabel": {},
"setCoverDialogTitle": "Set Cover",
"@setCoverDialogTitle": {},
"setCoverDialogLatest": "Latest item",

View file

@ -15,7 +15,7 @@ class Covers with ChangeNotifier {
Covers._private();
Future<void> init() async {
_rows = await metadataDb.loadCovers();
_rows = await metadataDb.loadAllCovers();
}
int get count => _rows.length;

View file

@ -12,7 +12,7 @@ class Favourites with ChangeNotifier {
Favourites._private();
Future<void> init() async {
_rows = await metadataDb.loadFavourites();
_rows = await metadataDb.loadAllFavourites();
}
int get count => _rows.length;

View file

@ -7,6 +7,7 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata_db_upgrade.dart';
import 'package:aves/model/video_playback.dart';
import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
@ -25,7 +26,7 @@ abstract class MetadataDb {
Future<void> clearEntries();
Future<Set<AvesEntry>> loadEntries();
Future<Set<AvesEntry>> loadAllEntries();
Future<void> saveEntries(Iterable<AvesEntry> entries);
@ -43,7 +44,7 @@ abstract class MetadataDb {
Future<void> clearMetadataEntries();
Future<List<CatalogMetadata>> loadMetadataEntries();
Future<List<CatalogMetadata>> loadAllMetadataEntries();
Future<void> saveMetadata(Set<CatalogMetadata> metadataEntries);
@ -53,7 +54,7 @@ abstract class MetadataDb {
Future<void> clearAddresses();
Future<List<AddressDetails>> loadAddresses();
Future<List<AddressDetails>> loadAllAddresses();
Future<void> saveAddresses(Set<AddressDetails> addresses);
@ -63,7 +64,7 @@ abstract class MetadataDb {
Future<void> clearFavourites();
Future<Set<FavouriteRow>> loadFavourites();
Future<Set<FavouriteRow>> loadAllFavourites();
Future<void> addFavourites(Iterable<FavouriteRow> rows);
@ -75,13 +76,27 @@ abstract class MetadataDb {
Future<void> clearCovers();
Future<Set<CoverRow>> loadCovers();
Future<Set<CoverRow>> loadAllCovers();
Future<void> addCovers(Iterable<CoverRow> rows);
Future<void> updateCoverEntryId(int oldId, CoverRow row);
Future<void> removeCovers(Set<CollectionFilter> filters);
// video playback
Future<void> clearVideoPlayback();
Future<Set<VideoPlaybackRow>> loadAllVideoPlayback();
Future<VideoPlaybackRow?> loadVideoPlayback(int? contentId);
Future<void> addVideoPlayback(Set<VideoPlaybackRow> rows);
Future<void> updateVideoPlaybackId(int oldId, int? newId);
Future<void> removeVideoPlayback(Set<int> contentIds);
}
class SqfliteMetadataDb implements MetadataDb {
@ -95,6 +110,7 @@ class SqfliteMetadataDb implements MetadataDb {
static const addressTable = 'address';
static const favouriteTable = 'favourites';
static const coverTable = 'covers';
static const videoPlaybackTable = 'videoPlayback';
@override
Future<void> init() async {
@ -146,9 +162,13 @@ class SqfliteMetadataDb implements MetadataDb {
'filter TEXT PRIMARY KEY'
', contentId INTEGER'
')');
await db.execute('CREATE TABLE $videoPlaybackTable('
'contentId INTEGER PRIMARY KEY'
', resumeTimeMillis INTEGER'
')');
},
onUpgrade: MetadataDbUpgrader.upgradeDb,
version: 4,
version: 5,
);
}
@ -183,6 +203,7 @@ class SqfliteMetadataDb implements MetadataDb {
if (!metadataOnly) {
batch.delete(favouriteTable, where: where, whereArgs: whereArgs);
batch.delete(coverTable, where: where, whereArgs: whereArgs);
batch.delete(videoPlaybackTable, where: where, whereArgs: whereArgs);
}
});
await batch.commit(noResult: true);
@ -194,11 +215,11 @@ class SqfliteMetadataDb implements MetadataDb {
Future<void> clearEntries() async {
final db = await _database;
final count = await db.delete(entryTable, where: '1');
debugPrint('$runtimeType clearEntries deleted $count entries');
debugPrint('$runtimeType clearEntries deleted $count rows');
}
@override
Future<Set<AvesEntry>> loadEntries() async {
Future<Set<AvesEntry>> loadAllEntries() async {
final db = await _database;
final maps = await db.query(entryTable);
final entries = maps.map((map) => AvesEntry.fromMap(map)).toSet();
@ -252,7 +273,7 @@ class SqfliteMetadataDb implements MetadataDb {
Future<void> clearDates() async {
final db = await _database;
final count = await db.delete(dateTakenTable, where: '1');
debugPrint('$runtimeType clearDates deleted $count entries');
debugPrint('$runtimeType clearDates deleted $count rows');
}
@override
@ -269,11 +290,11 @@ class SqfliteMetadataDb implements MetadataDb {
Future<void> clearMetadataEntries() async {
final db = await _database;
final count = await db.delete(metadataTable, where: '1');
debugPrint('$runtimeType clearMetadataEntries deleted $count entries');
debugPrint('$runtimeType clearMetadataEntries deleted $count rows');
}
@override
Future<List<CatalogMetadata>> loadMetadataEntries() async {
Future<List<CatalogMetadata>> loadAllMetadataEntries() async {
final db = await _database;
final maps = await db.query(metadataTable);
final metadataEntries = maps.map((map) => CatalogMetadata.fromMap(map)).toList();
@ -330,11 +351,11 @@ class SqfliteMetadataDb implements MetadataDb {
Future<void> clearAddresses() async {
final db = await _database;
final count = await db.delete(addressTable, where: '1');
debugPrint('$runtimeType clearAddresses deleted $count entries');
debugPrint('$runtimeType clearAddresses deleted $count rows');
}
@override
Future<List<AddressDetails>> loadAddresses() async {
Future<List<AddressDetails>> loadAllAddresses() async {
final db = await _database;
final maps = await db.query(addressTable);
final addresses = maps.map((map) => AddressDetails.fromMap(map)).toList();
@ -376,11 +397,11 @@ class SqfliteMetadataDb implements MetadataDb {
Future<void> clearFavourites() async {
final db = await _database;
final count = await db.delete(favouriteTable, where: '1');
debugPrint('$runtimeType clearFavourites deleted $count entries');
debugPrint('$runtimeType clearFavourites deleted $count rows');
}
@override
Future<Set<FavouriteRow>> loadFavourites() async {
Future<Set<FavouriteRow>> loadAllFavourites() async {
final db = await _database;
final maps = await db.query(favouriteTable);
final rows = maps.map((map) => FavouriteRow.fromMap(map)).toSet();
@ -432,11 +453,11 @@ class SqfliteMetadataDb implements MetadataDb {
Future<void> clearCovers() async {
final db = await _database;
final count = await db.delete(coverTable, where: '1');
debugPrint('$runtimeType clearCovers deleted $count entries');
debugPrint('$runtimeType clearCovers deleted $count rows');
}
@override
Future<Set<CoverRow>> loadCovers() async {
Future<Set<CoverRow>> loadAllCovers() async {
final db = await _database;
final maps = await db.query(coverTable);
final rows = maps.map(CoverRow.fromMap).whereNotNull().toSet();
@ -446,6 +467,7 @@ class SqfliteMetadataDb implements MetadataDb {
@override
Future<void> addCovers(Iterable<CoverRow> rows) async {
if (rows.isEmpty) return;
final db = await _database;
final batch = db.batch();
rows.forEach((row) => _batchInsertCover(batch, row));
@ -479,4 +501,71 @@ class SqfliteMetadataDb implements MetadataDb {
filters.forEach((filter) => batch.delete(coverTable, where: 'filter = ?', whereArgs: [filter.toJson()]));
await batch.commit(noResult: true);
}
// video playback
@override
Future<void> clearVideoPlayback() async {
final db = await _database;
final count = await db.delete(videoPlaybackTable, where: '1');
debugPrint('$runtimeType clearVideoPlayback deleted $count rows');
}
@override
Future<Set<VideoPlaybackRow>> loadAllVideoPlayback() async {
final db = await _database;
final maps = await db.query(videoPlaybackTable);
final rows = maps.map(VideoPlaybackRow.fromMap).whereNotNull().toSet();
return rows;
}
@override
Future<VideoPlaybackRow?> loadVideoPlayback(int? contentId) async {
if (contentId == null) return null;
final db = await _database;
final maps = await db.query(videoPlaybackTable, where: 'contentId = ?', whereArgs: [contentId]);
if (maps.isEmpty) return null;
return VideoPlaybackRow.fromMap(maps.first);
}
@override
Future<void> addVideoPlayback(Set<VideoPlaybackRow> rows) async {
if (rows.isEmpty) return;
final db = await _database;
final batch = db.batch();
rows.forEach((row) => _batchInsertVideoPlayback(batch, row));
await batch.commit(noResult: true);
}
void _batchInsertVideoPlayback(Batch batch, VideoPlaybackRow row) {
batch.insert(
videoPlaybackTable,
row.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
@override
Future<void> updateVideoPlaybackId(int oldId, int? newId) async {
if (newId != null) {
final db = await _database;
await db.update(videoPlaybackTable, {'contentId': newId}, where: 'contentId = ?', whereArgs: [oldId]);
} else {
await removeVideoPlayback({oldId});
}
}
@override
Future<void> removeVideoPlayback(Set<int> contentIds) async {
if (contentIds.isEmpty) return;
final db = await _database;
// using array in `whereArgs` and using it with `where filter IN ?` is a pain, so we prefer `batch` instead
final batch = db.batch();
contentIds.forEach((id) => batch.delete(videoPlaybackTable, where: 'contentId = ?', whereArgs: [id]));
await batch.commit(noResult: true);
}
}

View file

@ -6,6 +6,7 @@ class MetadataDbUpgrader {
static const entryTable = SqfliteMetadataDb.entryTable;
static const metadataTable = SqfliteMetadataDb.metadataTable;
static const coverTable = SqfliteMetadataDb.coverTable;
static const videoPlaybackTable = SqfliteMetadataDb.videoPlaybackTable;
// warning: "ALTER TABLE ... RENAME COLUMN ..." is not supported
// on SQLite <3.25.0, bundled on older Android devices
@ -21,6 +22,9 @@ class MetadataDbUpgrader {
case 3:
await _upgradeFrom3(db);
break;
case 4:
await _upgradeFrom4(db);
break;
}
oldVersion++;
}
@ -109,4 +113,12 @@ class MetadataDbUpgrader {
', contentId INTEGER'
')');
}
static Future<void> _upgradeFrom4(Database db) async {
debugPrint('upgrading DB from v4');
await db.execute('CREATE TABLE $videoPlaybackTable('
'contentId INTEGER PRIMARY KEY'
', resumeTimeMillis INTEGER'
')');
}
}

View file

@ -119,6 +119,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
final entries = _rawEntries.where((entry) => uris.contains(entry.uri)).toSet();
await favourites.remove(entries);
await covers.removeEntries(entries);
await metadataDb.removeVideoPlayback(entries.map((entry) => entry.contentId).whereNotNull().toSet());
entries.forEach((v) => _entryById.remove(v.contentId));
_rawEntries.removeAll(entries);
@ -157,6 +158,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
await metadataDb.updateAddressId(oldContentId, entry.addressDetails);
await favourites.moveEntry(oldContentId, entry);
await covers.moveEntry(oldContentId, entry);
await metadataDb.updateVideoPlaybackId(oldContentId, entry.contentId);
}
}

View file

@ -21,7 +21,7 @@ mixin LocationMixin on SourceBase {
List<String> sortedPlaces = List.unmodifiable([]);
Future<void> loadAddresses() async {
final saved = await metadataDb.loadAddresses();
final saved = await metadataDb.loadAllAddresses();
final idMap = entryById;
saved.forEach((metadata) => idMap[metadata.contentId]?.addressDetails = metadata);
onAddressMetadataChanged();

View file

@ -51,7 +51,7 @@ class MediaStoreSource extends CollectionSource {
clearEntries();
debugPrint('$runtimeType refresh ${stopwatch.elapsed} fetch known entries');
final oldEntries = await metadataDb.loadEntries();
final oldEntries = await metadataDb.loadAllEntries();
debugPrint('$runtimeType refresh ${stopwatch.elapsed} check obsolete entries');
final knownDateById = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId!, entry.dateModifiedSecs!)));
final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(knownDateById.keys.toList())).toSet();

View file

@ -15,7 +15,7 @@ mixin TagMixin on SourceBase {
List<String> sortedTags = List.unmodifiable([]);
Future<void> loadCatalogMetadata() async {
final saved = await metadataDb.loadMetadataEntries();
final saved = await metadataDb.loadAllMetadataEntries();
final idMap = entryById;
saved.forEach((metadata) => idMap[metadata.contentId]?.catalogMetadata = metadata);
onCatalogMetadataChanged();

View file

@ -0,0 +1,27 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
@immutable
class VideoPlaybackRow extends Equatable {
final int contentId, resumeTimeMillis;
@override
List<Object?> get props => [contentId, resumeTimeMillis];
const VideoPlaybackRow({
required this.contentId,
required this.resumeTimeMillis,
});
static VideoPlaybackRow? fromMap(Map map) {
return VideoPlaybackRow(
contentId: map['contentId'],
resumeTimeMillis: map['resumeTimeMillis'],
);
}
Map<String, dynamic> toMap() => {
'contentId': contentId,
'resumeTimeMillis': resumeTimeMillis,
};
}

View file

@ -3,6 +3,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/video_playback.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/file_utils.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
@ -23,6 +24,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
late Future<List<AddressDetails>> _dbAddressLoader;
late Future<Set<FavouriteRow>> _dbFavouritesLoader;
late Future<Set<CoverRow>> _dbCoversLoader;
late Future<Set<VideoPlaybackRow>> _dbVideoPlaybackLoader;
@override
void initState() {
@ -188,6 +190,27 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
);
},
),
FutureBuilder<Set>(
future: _dbVideoPlaybackLoader,
builder: (context, snapshot) {
if (snapshot.hasError) return Text(snapshot.error.toString());
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
return Row(
children: [
Expanded(
child: Text('video playback rows: ${snapshot.data!.length}'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => metadataDb.clearVideoPlayback().then((_) => _startDbReport()),
child: const Text('Clear'),
),
],
);
},
),
],
),
),
@ -197,12 +220,13 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
void _startDbReport() {
_dbFileSizeLoader = metadataDb.dbFileSize();
_dbEntryLoader = metadataDb.loadEntries();
_dbEntryLoader = metadataDb.loadAllEntries();
_dbDateLoader = metadataDb.loadDates();
_dbMetadataLoader = metadataDb.loadMetadataEntries();
_dbAddressLoader = metadataDb.loadAddresses();
_dbFavouritesLoader = metadataDb.loadFavourites();
_dbCoversLoader = metadataDb.loadCovers();
_dbMetadataLoader = metadataDb.loadAllMetadataEntries();
_dbAddressLoader = metadataDb.loadAllAddresses();
_dbFavouritesLoader = metadataDb.loadAllFavourites();
_dbCoversLoader = metadataDb.loadAllCovers();
_dbVideoPlaybackLoader = metadataDb.loadAllVideoPlayback();
setState(() {});
}

View file

@ -1,6 +1,7 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/video_playback.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/viewer/info/common.dart';
import 'package:collection/collection.dart';
@ -23,6 +24,7 @@ class _DbTabState extends State<DbTab> {
late Future<AvesEntry?> _dbEntryLoader;
late Future<CatalogMetadata?> _dbMetadataLoader;
late Future<AddressDetails?> _dbAddressLoader;
late Future<VideoPlaybackRow?> _dbVideoPlaybackLoader;
AvesEntry get entry => widget.entry;
@ -35,9 +37,10 @@ class _DbTabState extends State<DbTab> {
void _loadDatabase() {
final contentId = entry.contentId;
_dbDateLoader = metadataDb.loadDates().then((values) => values[contentId]);
_dbEntryLoader = metadataDb.loadEntries().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId));
_dbMetadataLoader = metadataDb.loadMetadataEntries().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId));
_dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId));
_dbEntryLoader = metadataDb.loadAllEntries().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId));
_dbMetadataLoader = metadataDb.loadAllMetadataEntries().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId));
_dbAddressLoader = metadataDb.loadAllAddresses().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId));
_dbVideoPlaybackLoader = metadataDb.loadVideoPlayback(contentId);
setState(() {});
}
@ -150,6 +153,27 @@ class _DbTabState extends State<DbTab> {
);
},
),
const SizedBox(height: 16),
FutureBuilder<VideoPlaybackRow?>(
future: _dbVideoPlaybackLoader,
builder: (context, snapshot) {
if (snapshot.hasError) return Text(snapshot.error.toString());
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
final data = snapshot.data;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('DB video playback:${data == null ? ' no row' : ''}'),
if (data != null)
InfoRowGroup(
info: {
'resumeTimeMillis': '${data.resumeTimeMillis}',
},
),
],
);
},
),
],
);
}

View file

@ -597,7 +597,8 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
setState(() {});
if (settings.enableVideoAutoPlay) {
await _playVideo(controller, () => entry == _entryNotifier.value);
final resumeTimeMillis = await controller.getResumeTime(context);
await _playVideo(controller, () => entry == _entryNotifier.value, resumeTimeMillis: resumeTimeMillis);
}
}
@ -649,13 +650,17 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
}
}
Future<void> _playVideo(AvesVideoController videoController, bool Function() isCurrent) async {
Future<void> _playVideo(AvesVideoController videoController, bool Function() isCurrent, {int? resumeTimeMillis}) async {
// video decoding may fail or have initial artifacts when the player initializes
// during this widget initialization (because of the page transition and hero animation?)
// so we play after a delay for increased stability
await Future.delayed(const Duration(milliseconds: 300) * timeDilation);
if (resumeTimeMillis != null) {
await videoController.seekTo(resumeTimeMillis);
} else {
await videoController.play();
}
// playing controllers are paused when the entry changes,
// but the controller may still be preparing (not yet playing) when this happens

View file

@ -1,6 +1,11 @@
import 'dart:typed_data';
import 'package:aves/model/entry.dart';
import 'package:aves/model/video_playback.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/format.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -11,7 +16,65 @@ abstract class AvesVideoController {
AvesVideoController(AvesEntry entry) : _entry = entry;
Future<void> dispose();
static const resumeTimeSaveMinProgress = .05;
static const resumeTimeSaveMaxProgress = .95;
static const resumeTimeSaveMinDuration = Duration(minutes: 2);
@mustCallSuper
Future<void> dispose() async {
await _savePlaybackState();
}
Future<void> _savePlaybackState() async {
final contentId = entry.contentId;
if (contentId == null || !isReady || duration < resumeTimeSaveMinDuration.inMilliseconds) return;
final _progress = progress;
if (resumeTimeSaveMinProgress < _progress && _progress < resumeTimeSaveMaxProgress) {
await metadataDb.addVideoPlayback({
VideoPlaybackRow(
contentId: contentId,
resumeTimeMillis: currentPosition,
)
});
} else {
await metadataDb.removeVideoPlayback({contentId});
}
}
Future<int?> getResumeTime(BuildContext context) async {
final contentId = entry.contentId;
if (contentId == null) return null;
final playback = await metadataDb.loadVideoPlayback(contentId);
final resumeTime = playback?.resumeTimeMillis ?? 0;
if (resumeTime == 0) return null;
// clear on retrieval
await metadataDb.removeVideoPlayback({contentId});
final resume = await showDialog<bool>(
context: context,
builder: (context) {
return AvesDialog(
context: context,
content: Text(context.l10n.videoResumeDialogMessage(formatFriendlyDuration(Duration(milliseconds: resumeTime)))),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.videoStartOverButtonLabel),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text(context.l10n.videoResumeButtonLabel),
),
],
);
},
);
if (resume == null || !resume) return 0;
return resumeTime;
}
Future<void> play();

View file

@ -69,6 +69,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
@override
Future<void> dispose() async {
await super.dispose();
_initialPlayTimer?.cancel();
_stopListening();
await _valueStreamController.close();

View file

@ -184,8 +184,13 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
Future<void> _togglePlayPause(BuildContext context, AvesVideoController controller) async {
if (controller.isPlaying) {
await controller.pause();
} else {
final resumeTimeMillis = await controller.getResumeTime(context);
if (resumeTimeMillis != null) {
await controller.seekTo(resumeTimeMillis);
} else {
await controller.play();
}
// hide overlay
_overlayHidingTimer = Timer(context.read<DurationsData>().iconAnimation + Durations.videoOverlayHideDelay, () {
const ToggleOverlayNotification(visible: false).dispatch(context);

View file

@ -15,8 +15,10 @@ class FakeMetadataDb extends Fake implements MetadataDb {
@override
Future<void> removeIds(Set<int> contentIds, {required bool metadataOnly}) => SynchronousFuture(null);
// entries
@override
Future<Set<AvesEntry>> loadEntries() => SynchronousFuture({});
Future<Set<AvesEntry>> loadAllEntries() => SynchronousFuture({});
@override
Future<void> saveEntries(Iterable<AvesEntry> entries) => SynchronousFuture(null);
@ -24,11 +26,15 @@ class FakeMetadataDb extends Fake implements MetadataDb {
@override
Future<void> updateEntryId(int oldId, AvesEntry entry) => SynchronousFuture(null);
// date taken
@override
Future<Map<int?, int?>> loadDates() => SynchronousFuture({});
// catalog metadata
@override
Future<List<CatalogMetadata>> loadMetadataEntries() => SynchronousFuture([]);
Future<List<CatalogMetadata>> loadAllMetadataEntries() => SynchronousFuture([]);
@override
Future<void> saveMetadata(Set<CatalogMetadata> metadataEntries) => SynchronousFuture(null);
@ -36,8 +42,10 @@ class FakeMetadataDb extends Fake implements MetadataDb {
@override
Future<void> updateMetadataId(int oldId, CatalogMetadata? metadata) => SynchronousFuture(null);
// address
@override
Future<List<AddressDetails>> loadAddresses() => SynchronousFuture([]);
Future<List<AddressDetails>> loadAllAddresses() => SynchronousFuture([]);
@override
Future<void> saveAddresses(Set<AddressDetails> addresses) => SynchronousFuture(null);
@ -45,8 +53,10 @@ class FakeMetadataDb extends Fake implements MetadataDb {
@override
Future<void> updateAddressId(int oldId, AddressDetails? address) => SynchronousFuture(null);
// favourites
@override
Future<Set<FavouriteRow>> loadFavourites() => SynchronousFuture({});
Future<Set<FavouriteRow>> loadAllFavourites() => SynchronousFuture({});
@override
Future<void> addFavourites(Iterable<FavouriteRow> rows) => SynchronousFuture(null);
@ -57,8 +67,10 @@ class FakeMetadataDb extends Fake implements MetadataDb {
@override
Future<void> removeFavourites(Iterable<FavouriteRow> rows) => SynchronousFuture(null);
// covers
@override
Future<Set<CoverRow>> loadCovers() => SynchronousFuture({});
Future<Set<CoverRow>> loadAllCovers() => SynchronousFuture({});
@override
Future<void> addCovers(Iterable<CoverRow> rows) => SynchronousFuture(null);

View file

@ -2,6 +2,9 @@
"ko": [
"unsupportedTypeDialogTitle",
"unsupportedTypeDialogMessage",
"videoResumeDialogMessage",
"videoStartOverButtonLabel",
"videoResumeButtonLabel",
"editEntryDateDialogExtractFromTitle",
"collectionActionShowTitleSearch",
"collectionActionHideTitleSearch",
@ -16,6 +19,9 @@
"ru": [
"unsupportedTypeDialogTitle",
"unsupportedTypeDialogMessage",
"videoResumeDialogMessage",
"videoStartOverButtonLabel",
"videoResumeButtonLabel",
"editEntryDateDialogExtractFromTitle",
"aboutLinkPolicy",
"aboutCreditsTranslators",