#156 export/import covers & favourites

dart 2.15 static analysis
This commit is contained in:
Thibault Deckers 2022-01-19 16:15:40 +09:00
parent ca991ae9dd
commit c9041c9beb
43 changed files with 425 additions and 102 deletions

View file

@ -354,6 +354,10 @@
"settingsActionExport": "Exportieren", "settingsActionExport": "Exportieren",
"settingsActionImport": "Importieren", "settingsActionImport": "Importieren",
"appExportCovers": "Titelbilder",
"appExportFavourites": "Favoriten",
"appExportSettings": "Einstellungen",
"settingsSectionNavigation": "Navigation", "settingsSectionNavigation": "Navigation",
"settingsHome": "Startseite", "settingsHome": "Startseite",
"settingsKeepScreenOnTile": "Bildschirm eingeschaltet lassen", "settingsKeepScreenOnTile": "Bildschirm eingeschaltet lassen",

View file

@ -523,6 +523,10 @@
"settingsActionExport": "Export", "settingsActionExport": "Export",
"settingsActionImport": "Import", "settingsActionImport": "Import",
"appExportCovers": "Covers",
"appExportFavourites": "Favourites",
"appExportSettings": "Settings",
"settingsSectionNavigation": "Navigation", "settingsSectionNavigation": "Navigation",
"settingsHome": "Home", "settingsHome": "Home",
"settingsKeepScreenOnTile": "Keep screen on", "settingsKeepScreenOnTile": "Keep screen on",

View file

@ -40,7 +40,7 @@
"chipActionPin": "Fijar", "chipActionPin": "Fijar",
"chipActionUnpin": "Dejar de fijar", "chipActionUnpin": "Dejar de fijar",
"chipActionRename": "Renombrar", "chipActionRename": "Renombrar",
"chipActionSetCover": "Elegir portada", "chipActionSetCover": "Elegir carátula",
"chipActionCreateAlbum": "Crear álbum", "chipActionCreateAlbum": "Crear álbum",
"entryActionCopyToClipboard": "Copiar al portapapeles", "entryActionCopyToClipboard": "Copiar al portapapeles",
@ -355,6 +355,10 @@
"settingsActionExport": "Exportar", "settingsActionExport": "Exportar",
"settingsActionImport": "Importar", "settingsActionImport": "Importar",
"appExportCovers": "Carátulas",
"appExportFavourites": "Favoritos",
"appExportSettings": "Ajustes",
"settingsSectionNavigation": "Navegación", "settingsSectionNavigation": "Navegación",
"settingsHome": "Inicio", "settingsHome": "Inicio",
"settingsKeepScreenOnTile": "Mantener pantalla encendida", "settingsKeepScreenOnTile": "Mantener pantalla encendida",

View file

@ -354,6 +354,10 @@
"settingsActionExport": "Exporter", "settingsActionExport": "Exporter",
"settingsActionImport": "Importer", "settingsActionImport": "Importer",
"appExportCovers": "Couvertures",
"appExportFavourites": "Favoris",
"appExportSettings": "Réglages",
"settingsSectionNavigation": "Navigation", "settingsSectionNavigation": "Navigation",
"settingsHome": "Page daccueil", "settingsHome": "Page daccueil",
"settingsKeepScreenOnTile": "Maintenir lécran allumé", "settingsKeepScreenOnTile": "Maintenir lécran allumé",

View file

@ -354,6 +354,10 @@
"settingsActionExport": "내보내기", "settingsActionExport": "내보내기",
"settingsActionImport": "가져오기", "settingsActionImport": "가져오기",
"appExportCovers": "대표 이미지",
"appExportFavourites": "즐겨찾기",
"appExportSettings": "설정",
"settingsSectionNavigation": "탐색", "settingsSectionNavigation": "탐색",
"settingsHome": "홈", "settingsHome": "홈",
"settingsKeepScreenOnTile": "화면 자동 꺼짐 방지", "settingsKeepScreenOnTile": "화면 자동 꺼짐 방지",

View file

@ -354,6 +354,10 @@
"settingsActionExport": "Exportar", "settingsActionExport": "Exportar",
"settingsActionImport": "Importar", "settingsActionImport": "Importar",
"appExportCovers": "Capas",
"appExportFavourites": "Favoritos",
"appExportSettings": "Configurações",
"settingsSectionNavigation": "Navegação", "settingsSectionNavigation": "Navegação",
"settingsHome": "Início", "settingsHome": "Início",
"settingsKeepScreenOnTile": "Manter a tela ligada", "settingsKeepScreenOnTile": "Manter a tela ligada",

View file

@ -354,6 +354,9 @@
"settingsActionExport": "Экспорт", "settingsActionExport": "Экспорт",
"settingsActionImport": "Импорт", "settingsActionImport": "Импорт",
"appExportFavourites": "Избранное",
"appExportSettings": "Настройки",
"settingsSectionNavigation": "Навигация", "settingsSectionNavigation": "Навигация",
"settingsHome": "Домашний каталог", "settingsHome": "Домашний каталог",
"settingsKeepScreenOnTile": "Держать экран включенным", "settingsKeepScreenOnTile": "Держать экран включенным",

View file

@ -1,11 +1,12 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
final Covers covers = Covers._private(); final Covers covers = Covers._private();
@ -20,6 +21,8 @@ class Covers with ChangeNotifier {
int get count => _rows.length; int get count => _rows.length;
Set<CoverRow> get all => Set.unmodifiable(_rows);
int? coverContentId(CollectionFilter filter) => _rows.firstWhereOrNull((row) => row.filter == filter)?.contentId; int? coverContentId(CollectionFilter filter) => _rows.firstWhereOrNull((row) => row.filter == filter)?.contentId;
Future<void> set(CollectionFilter filter, int? contentId) async { Future<void> set(CollectionFilter filter, int? contentId) async {
@ -75,6 +78,61 @@ class Covers with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
// import/export
List<Map<String, dynamic>>? export(CollectionSource source) {
final visibleEntries = source.visibleEntries;
final jsonList = covers.all
.map((row) {
final id = row.contentId;
final path = visibleEntries.firstWhereOrNull((entry) => id == entry.contentId)?.path;
if (path == null) return null;
final volume = androidFileUtils.getStorageVolume(path)?.path;
if (volume == null) return null;
final relativePath = path.substring(volume.length);
return {
'filter': row.filter.toJson(),
'volume': volume,
'relativePath': relativePath,
};
})
.whereNotNull()
.toList();
return jsonList.isNotEmpty ? jsonList : null;
}
void import(dynamic jsonList, CollectionSource source) {
if (jsonList is! List) {
debugPrint('failed to import covers for jsonMap=$jsonList');
return;
}
final visibleEntries = source.visibleEntries;
jsonList.forEach((row) {
final filter = CollectionFilter.fromJson(row['filter']);
if (filter == null) {
debugPrint('failed to import cover for row=$row');
return;
}
final volume = row['volume'];
final relativePath = row['relativePath'];
if (volume is String && relativePath is String) {
final path = pContext.join(volume, relativePath);
final entry = visibleEntries.firstWhereOrNull((entry) => entry.path == path && filter.test(entry));
if (entry != null) {
covers.set(filter, entry.contentId);
} else {
debugPrint('failed to import cover for path=$path, filter=$filter');
}
} else {
debugPrint('failed to import cover for volume=$volume, relativePath=$relativePath, filter=$filter');
}
});
}
} }
@immutable @immutable

View file

@ -57,7 +57,7 @@ extension ExtraAvesEntryImages on AvesEntry {
bool _isReady(Object providerKey) => imageCache!.statusForKey(providerKey).keepAlive; bool _isReady(Object providerKey) => imageCache!.statusForKey(providerKey).keepAlive;
List<ThumbnailProvider> get cachedThumbnails => EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).where(_isReady).map((key) => ThumbnailProvider(key)).toList(); List<ThumbnailProvider> get cachedThumbnails => EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).where(_isReady).map(ThumbnailProvider.new).toList();
ThumbnailProvider get bestCachedThumbnail { ThumbnailProvider get bestCachedThumbnail {
final sizedThumbnailKey = EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).firstWhereOrNull(_isReady); final sizedThumbnailKey = EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).firstWhereOrNull(_isReady);

View file

@ -1,5 +1,7 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -17,6 +19,8 @@ class Favourites with ChangeNotifier {
int get count => _rows.length; int get count => _rows.length;
Set<int> get all => Set.unmodifiable(_rows.map((v) => v.contentId));
bool isFavourite(AvesEntry entry) => _rows.any((row) => row.contentId == entry.contentId); bool isFavourite(AvesEntry entry) => _rows.any((row) => row.contentId == entry.contentId);
FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId!, path: entry.path!); FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId!, path: entry.path!);
@ -59,6 +63,56 @@ class Favourites with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
// import/export
Map<String, List<String>>? export(CollectionSource source) {
final visibleEntries = source.visibleEntries;
final ids = favourites.all;
final paths = visibleEntries.where((entry) => ids.contains(entry.contentId)).map((entry) => entry.path).whereNotNull().toSet();
final byVolume = groupBy<String, StorageVolume?>(paths, androidFileUtils.getStorageVolume);
final jsonMap = Map.fromEntries(byVolume.entries.map((kv) {
final volume = kv.key?.path;
if (volume == null) return null;
final rootLength = volume.length;
final relativePaths = kv.value.map((v) => v.substring(rootLength)).toList();
return MapEntry(volume, relativePaths);
}).whereNotNull());
return jsonMap.isNotEmpty ? jsonMap : null;
}
void import(dynamic jsonMap, CollectionSource source) {
if (jsonMap is! Map) {
debugPrint('failed to import favourites for jsonMap=$jsonMap');
return;
}
final visibleEntries = source.visibleEntries;
final foundEntries = <AvesEntry>{};
final missedPaths = <String>{};
jsonMap.forEach((volume, relativePaths) {
if (volume is String && relativePaths is List) {
relativePaths.forEach((relativePath) {
final path = pContext.join(volume, relativePath);
final entry = visibleEntries.firstWhereOrNull((entry) => entry.path == path);
if (entry != null) {
foundEntries.add(entry);
} else {
missedPaths.add(path);
}
});
} else {
debugPrint('failed to import favourites for volume=$volume, relativePaths=${relativePaths.runtimeType}');
}
if (foundEntries.isNotEmpty) {
favourites.add(foundEntries);
}
if (missedPaths.isNotEmpty) {
debugPrint('failed to import favourites with ${missedPaths.length} missed paths');
}
});
}
} }
@immutable @immutable

View file

@ -231,7 +231,7 @@ class SqfliteMetadataDb implements MetadataDb {
Future<Set<AvesEntry>> loadAllEntries() async { Future<Set<AvesEntry>> loadAllEntries() async {
final db = await _database; final db = await _database;
final maps = await db.query(entryTable); final maps = await db.query(entryTable);
final entries = maps.map((map) => AvesEntry.fromMap(map)).toSet(); final entries = maps.map(AvesEntry.fromMap).toSet();
return entries; return entries;
} }
@ -273,7 +273,7 @@ class SqfliteMetadataDb implements MetadataDb {
orderBy: 'sourceDateTakenMillis DESC', orderBy: 'sourceDateTakenMillis DESC',
limit: limit, limit: limit,
); );
return maps.map((map) => AvesEntry.fromMap(map)).toSet(); return maps.map(AvesEntry.fromMap).toSet();
} }
// date taken // date taken
@ -306,7 +306,7 @@ class SqfliteMetadataDb implements MetadataDb {
Future<List<CatalogMetadata>> loadAllMetadataEntries() async { Future<List<CatalogMetadata>> loadAllMetadataEntries() async {
final db = await _database; final db = await _database;
final maps = await db.query(metadataTable); final maps = await db.query(metadataTable);
final metadataEntries = maps.map((map) => CatalogMetadata.fromMap(map)).toList(); final metadataEntries = maps.map(CatalogMetadata.fromMap).toList();
return metadataEntries; return metadataEntries;
} }
@ -367,7 +367,7 @@ class SqfliteMetadataDb implements MetadataDb {
Future<List<AddressDetails>> loadAllAddresses() async { Future<List<AddressDetails>> loadAllAddresses() async {
final db = await _database; final db = await _database;
final maps = await db.query(addressTable); final maps = await db.query(addressTable);
final addresses = maps.map((map) => AddressDetails.fromMap(map)).toList(); final addresses = maps.map(AddressDetails.fromMap).toList();
return addresses; return addresses;
} }
@ -413,7 +413,7 @@ class SqfliteMetadataDb implements MetadataDb {
Future<Set<FavouriteRow>> loadAllFavourites() async { Future<Set<FavouriteRow>> loadAllFavourites() async {
final db = await _database; final db = await _database;
final maps = await db.query(favouriteTable); final maps = await db.query(favouriteTable);
final rows = maps.map((map) => FavouriteRow.fromMap(map)).toSet(); final rows = maps.map(FavouriteRow.fromMap).toSet();
return rows; return rows;
} }

View file

@ -36,7 +36,7 @@ class MultiPageInfo {
factory MultiPageInfo.fromPageMaps(AvesEntry mainEntry, List<Map> pageMaps) { factory MultiPageInfo.fromPageMaps(AvesEntry mainEntry, List<Map> pageMaps) {
return MultiPageInfo( return MultiPageInfo(
mainEntry: mainEntry, mainEntry: mainEntry,
pages: pageMaps.map((page) => SinglePageInfo.fromMap(page)).toList(), pages: pageMaps.map(SinglePageInfo.fromMap).toList(),
); );
} }

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:aves/l10n/l10n.dart'; import 'package:aves/l10n/l10n.dart';
@ -570,12 +569,11 @@ class Settings extends ChangeNotifier {
// import/export // import/export
String toJson() => jsonEncode(Map.fromEntries( Map<String, dynamic> export() => Map.fromEntries(
_prefs!.getKeys().whereNot(internalKeys.contains).map((k) => MapEntry(k, _prefs!.get(k))), _prefs!.getKeys().whereNot(internalKeys.contains).map((k) => MapEntry(k, _prefs!.get(k))),
)); );
Future<void> fromJson(String jsonString) async { Future<void> import(dynamic jsonMap) async {
final jsonMap = jsonDecode(jsonString);
if (jsonMap is Map<String, dynamic>) { if (jsonMap is Map<String, dynamic>) {
// clear to restore defaults // clear to restore defaults
await reset(includeInternalKeys: false); await reset(includeInternalKeys: false);

View file

@ -8,4 +8,4 @@ enum EntrySortFactor { date, name, rating, size }
enum EntryGroupFactor { none, album, month, day } enum EntryGroupFactor { none, album, month, day }
enum TileLayout { grid, list } enum TileLayout { grid, list }

View file

@ -3,4 +3,4 @@ class IPTC {
// ApplicationRecord tags // ApplicationRecord tags
static const int keywordsTag = 25; static const int keywordsTag = 25;
} }

View file

@ -38,7 +38,7 @@ class PlatformAndroidAppService implements AndroidAppService {
Future<Set<Package>> getPackages() async { Future<Set<Package>> getPackages() async {
try { try {
final result = await platform.invokeMethod('getPackages'); final result = await platform.invokeMethod('getPackages');
final packages = (result as List).cast<Map>().map((map) => Package.fromMap(map)).toSet(); final packages = (result as List).cast<Map>().map(Package.fromMap).toSet();
// additional info for known directories // additional info for known directories
final kakaoTalk = packages.firstWhereOrNull((package) => package.packageName == 'com.kakao.talk'); final kakaoTalk = packages.firstWhereOrNull((package) => package.packageName == 'com.kakao.talk');
if (kakaoTalk != null) { if (kakaoTalk != null) {

View file

@ -66,7 +66,7 @@ class ServicePolicy {
} }
} }
LinkedHashMap<Object, _Task> _getQueue(int priority) => _queues.putIfAbsent(priority, () => LinkedHashMap()); LinkedHashMap<Object, _Task> _getQueue(int priority) => _queues.putIfAbsent(priority, LinkedHashMap.new);
void _pickNext() { void _pickNext() {
_notifyQueueState(); _notifyQueueState();

View file

@ -32,18 +32,18 @@ final StorageService storageService = getIt<StorageService>();
final WindowService windowService = getIt<WindowService>(); final WindowService windowService = getIt<WindowService>();
void initPlatformServices() { void initPlatformServices() {
getIt.registerLazySingleton<p.Context>(() => p.Context()); getIt.registerLazySingleton<p.Context>(p.Context.new);
getIt.registerLazySingleton<AvesAvailability>(() => LiveAvesAvailability()); getIt.registerLazySingleton<AvesAvailability>(LiveAvesAvailability.new);
getIt.registerLazySingleton<MetadataDb>(() => SqfliteMetadataDb()); getIt.registerLazySingleton<MetadataDb>(SqfliteMetadataDb.new);
getIt.registerLazySingleton<AndroidAppService>(() => PlatformAndroidAppService()); getIt.registerLazySingleton<AndroidAppService>(PlatformAndroidAppService.new);
getIt.registerLazySingleton<DeviceService>(() => PlatformDeviceService()); getIt.registerLazySingleton<DeviceService>(PlatformDeviceService.new);
getIt.registerLazySingleton<EmbeddedDataService>(() => PlatformEmbeddedDataService()); getIt.registerLazySingleton<EmbeddedDataService>(PlatformEmbeddedDataService.new);
getIt.registerLazySingleton<MediaFileService>(() => PlatformMediaFileService()); getIt.registerLazySingleton<MediaFileService>(PlatformMediaFileService.new);
getIt.registerLazySingleton<MediaStoreService>(() => PlatformMediaStoreService()); getIt.registerLazySingleton<MediaStoreService>(PlatformMediaStoreService.new);
getIt.registerLazySingleton<MetadataEditService>(() => PlatformMetadataEditService()); getIt.registerLazySingleton<MetadataEditService>(PlatformMetadataEditService.new);
getIt.registerLazySingleton<MetadataFetchService>(() => PlatformMetadataFetchService()); getIt.registerLazySingleton<MetadataFetchService>(PlatformMetadataFetchService.new);
getIt.registerLazySingleton<ReportService>(() => PlatformReportService()); getIt.registerLazySingleton<ReportService>(PlatformReportService.new);
getIt.registerLazySingleton<StorageService>(() => PlatformStorageService()); getIt.registerLazySingleton<StorageService>(PlatformStorageService.new);
getIt.registerLazySingleton<WindowService>(() => PlatformWindowService()); getIt.registerLazySingleton<WindowService>(PlatformWindowService.new);
} }

View file

@ -20,7 +20,7 @@ class GeocodingService {
// returns nothing with `maxResults` of 1, but succeeds with `maxResults` of 2+ // returns nothing with `maxResults` of 1, but succeeds with `maxResults` of 2+
'maxResults': 2, 'maxResults': 2,
}); });
return (result as List).cast<Map>().map((map) => Address.fromMap(map)).toList(); return (result as List).cast<Map>().map(Address.fromMap).toList();
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
if (e.code != 'getAddress-empty' && e.code != 'getAddress-network') { if (e.code != 'getAddress-empty' && e.code != 'getAddress-network') {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);

View file

@ -325,11 +325,14 @@ class PlatformMediaFileService implements MediaFileService {
required Iterable<AvesEntry> entries, required Iterable<AvesEntry> entries,
}) { }) {
try { try {
return _opStreamChannel.receiveBroadcastStream(<String, dynamic>{ return _opStreamChannel
'op': 'delete', .receiveBroadcastStream(<String, dynamic>{
'id': opId, 'op': 'delete',
'entries': entries.map(_toPlatformEntryMap).toList(), 'id': opId,
}).map((event) => ImageOpEvent.fromMap(event)); 'entries': entries.map(_toPlatformEntryMap).toList(),
})
.where((event) => event is Map)
.map((event) => ImageOpEvent.fromMap(event as Map));
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
reportService.recordError(e, stack); reportService.recordError(e, stack);
return Stream.error(e); return Stream.error(e);
@ -345,14 +348,17 @@ class PlatformMediaFileService implements MediaFileService {
required NameConflictStrategy nameConflictStrategy, required NameConflictStrategy nameConflictStrategy,
}) { }) {
try { try {
return _opStreamChannel.receiveBroadcastStream(<String, dynamic>{ return _opStreamChannel
'op': 'move', .receiveBroadcastStream(<String, dynamic>{
'id': opId, 'op': 'move',
'entries': entries.map(_toPlatformEntryMap).toList(), 'id': opId,
'copy': copy, 'entries': entries.map(_toPlatformEntryMap).toList(),
'destinationPath': destinationAlbum, 'copy': copy,
'nameConflictStrategy': nameConflictStrategy.toPlatform(), 'destinationPath': destinationAlbum,
}).map((event) => MoveOpEvent.fromMap(event)); 'nameConflictStrategy': nameConflictStrategy.toPlatform(),
})
.where((event) => event is Map)
.map((event) => MoveOpEvent.fromMap(event as Map));
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
reportService.recordError(e, stack); reportService.recordError(e, stack);
return Stream.error(e); return Stream.error(e);
@ -367,13 +373,16 @@ class PlatformMediaFileService implements MediaFileService {
required NameConflictStrategy nameConflictStrategy, required NameConflictStrategy nameConflictStrategy,
}) { }) {
try { try {
return _opStreamChannel.receiveBroadcastStream(<String, dynamic>{ return _opStreamChannel
'op': 'export', .receiveBroadcastStream(<String, dynamic>{
'entries': entries.map(_toPlatformEntryMap).toList(), 'op': 'export',
'mimeType': mimeType, 'entries': entries.map(_toPlatformEntryMap).toList(),
'destinationPath': destinationAlbum, 'mimeType': mimeType,
'nameConflictStrategy': nameConflictStrategy.toPlatform(), 'destinationPath': destinationAlbum,
}).map((event) => ExportOpEvent.fromMap(event)); 'nameConflictStrategy': nameConflictStrategy.toPlatform(),
})
.where((event) => event is Map)
.map((event) => ExportOpEvent.fromMap(event as Map));
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
reportService.recordError(e, stack); reportService.recordError(e, stack);
return Stream.error(e); return Stream.error(e);
@ -386,11 +395,14 @@ class PlatformMediaFileService implements MediaFileService {
required String newName, required String newName,
}) { }) {
try { try {
return _opStreamChannel.receiveBroadcastStream(<String, dynamic>{ return _opStreamChannel
'op': 'rename', .receiveBroadcastStream(<String, dynamic>{
'entries': entries.map(_toPlatformEntryMap).toList(), 'op': 'rename',
'newName': newName, 'entries': entries.map(_toPlatformEntryMap).toList(),
}).map((event) => MoveOpEvent.fromMap(event)); 'newName': newName,
})
.where((event) => event is Map)
.map((event) => MoveOpEvent.fromMap(event as Map));
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
reportService.recordError(e, stack); reportService.recordError(e, stack);
return Stream.error(e); return Stream.error(e);

View file

@ -50,9 +50,12 @@ class PlatformMediaStoreService implements MediaStoreService {
@override @override
Stream<AvesEntry> getEntries(Map<int, int> knownEntries) { Stream<AvesEntry> getEntries(Map<int, int> knownEntries) {
try { try {
return _streamChannel.receiveBroadcastStream(<String, dynamic>{ return _streamChannel
'knownEntries': knownEntries, .receiveBroadcastStream(<String, dynamic>{
}).map((event) => AvesEntry.fromMap(event)); 'knownEntries': knownEntries,
})
.where((event) => event is Map)
.map((event) => AvesEntry.fromMap(event as Map));
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
reportService.recordError(e, stack); reportService.recordError(e, stack);
return Stream.error(e); return Stream.error(e);

View file

@ -47,7 +47,7 @@ class PlatformStorageService implements StorageService {
Future<Set<StorageVolume>> getStorageVolumes() async { Future<Set<StorageVolume>> getStorageVolumes() async {
try { try {
final result = await platform.invokeMethod('getStorageVolumes'); final result = await platform.invokeMethod('getStorageVolumes');
return (result as List).cast<Map>().map((map) => StorageVolume.fromMap(map)).toSet(); return (result as List).cast<Map>().map(StorageVolume.fromMap).toSet();
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }

View file

@ -65,7 +65,7 @@ class MonthSectionHeader<T> extends StatelessWidget {
if (date == null) return l10n.sectionUnknown; if (date == null) return l10n.sectionUnknown;
if (date.isThisMonth) return l10n.dateThisMonth; if (date.isThisMonth) return l10n.dateThisMonth;
final locale = l10n.localeName; final locale = l10n.localeName;
final localized = date.isThisYear? DateFormat.MMMM(locale).format(date) : DateFormat.yMMMM(locale).format(date); final localized = date.isThisYear ? DateFormat.MMMM(locale).format(date) : DateFormat.yMMMM(locale).format(date);
return '${localized.substring(0, 1).toUpperCase()}${localized.substring(1)}'; return '${localized.substring(0, 1).toUpperCase()}${localized.substring(1)}';
} }

View file

@ -320,7 +320,7 @@ class _OverlayCoordinateFilterChipState extends State<_OverlayCoordinateFilterCh
filter: filter, filter: filter,
useFilterColor: false, useFilterColor: false,
maxWidth: double.infinity, maxWidth: double.infinity,
onTap: (filter) => FilterSelectedNotification(CoordinateFilter(bounds.sw, bounds.ne) ).dispatch(context), onTap: (filter) => FilterSelectedNotification(CoordinateFilter(bounds.sw, bounds.ne)).dispatch(context),
), ),
), ),
); );

View file

@ -293,7 +293,7 @@ class _GeoMapState extends State<GeoMap> {
// node size: 64 by default, higher means faster indexing but slower search // node size: 64 by default, higher means faster indexing but slower search
nodeSize: nodeSize, nodeSize: nodeSize,
points: markers, points: markers,
createCluster: (base, lng, lat) => GeoEntry.createCluster(base, lng, lat), createCluster: GeoEntry.createCluster,
); );
} }

View file

@ -4,7 +4,6 @@ import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
class MediaStoreScanDirDialog extends StatefulWidget { class MediaStoreScanDirDialog extends StatefulWidget {
const MediaStoreScanDirDialog({Key? key}) : super(key: key); const MediaStoreScanDirDialog({Key? key}) : super(key: key);
@ -37,7 +36,7 @@ class _MediaStoreScanDirDialogState extends State<MediaStoreScanDirDialog> {
setState(() => _processing = true); setState(() => _processing = true);
await Future.forEach<FileSystemEntity>(Directory(dir).listSync(recursive: true), (file) async { await Future.forEach<FileSystemEntity>(Directory(dir).listSync(recursive: true), (file) async {
if (file is File) { if (file is File) {
final mimeType = MimeTypes.forExtension(p.extension(file.path)); final mimeType = MimeTypes.forExtension(pContext.extension(file.path));
await mediaStoreService.scanFile(file.path, mimeType!); await mediaStoreService.scanFile(file.path, mimeType!);
} }
}); });

View file

@ -57,7 +57,7 @@ class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog<T>> {
if (needConfirmation) if (needConfirmation)
TextButton( TextButton(
onPressed: () => Navigator.pop(context, _selectedValue), onPressed: () => Navigator.pop(context, _selectedValue),
child: Text(confirmationButtonLabel!), child: Text(confirmationButtonLabel),
), ),
], ],
); );

View file

@ -77,8 +77,8 @@ class _TagEditorPageState extends State<TagEditorPage> {
builder: (context, value, child) { builder: (context, value, child) {
final upQuery = value.text.trim().toUpperCase(); final upQuery = value.text.trim().toUpperCase();
bool containQuery(String s) => s.toUpperCase().contains(upQuery); bool containQuery(String s) => s.toUpperCase().contains(upQuery);
final recentFilters = _recentTags.where(containQuery).map((v) => TagFilter(v)).toList(); final recentFilters = _recentTags.where(containQuery).map(TagFilter.new).toList();
final topTagFilters = _topTags.where(containQuery).map((v) => TagFilter(v)).toList(); final topTagFilters = _topTags.where(containQuery).map(TagFilter.new).toList();
return ListView( return ListView(
children: [ children: [
Padding( Padding(

View file

@ -54,7 +54,7 @@ class FilterListDetails<T extends CollectionFilter> extends StatelessWidget {
padding: const EdgeInsets.only(right: FilterListDetailsTheme.titleIconPadding), padding: const EdgeInsets.only(right: FilterListDetailsTheme.titleIconPadding),
child: IconTheme( child: IconTheme(
data: IconThemeData(color: detailsTheme.titleStyle.color), data: IconThemeData(color: detailsTheme.titleStyle.color),
child: leading!, child: leading,
), ),
), ),
), ),

View file

@ -53,7 +53,7 @@ class TagListPage extends StatelessWidget {
} }
List<FilterGridItem<TagFilter>> _getGridItems(CollectionSource source) { List<FilterGridItem<TagFilter>> _getGridItems(CollectionSource source) {
final filters = source.sortedTags.map((tag) => TagFilter(tag)).toSet(); final filters = source.sortedTags.map(TagFilter.new).toSet();
return FilterNavigationPage.sort(settings.tagSortFactor, source, filters); return FilterNavigationPage.sort(settings.tagSortFactor, source, filters);
} }

View file

@ -171,7 +171,7 @@ class CollectionSearchDelegate {
StreamBuilder( StreamBuilder(
stream: source.eventBus.on<TagsChangedEvent>(), stream: source.eventBus.on<TagsChangedEvent>(),
builder: (context, snapshot) { builder: (context, snapshot) {
final filters = source.sortedTags.where(containQuery).map((s) => TagFilter(s)); final filters = source.sortedTags.where(containQuery).map(TagFilter.new);
final noFilter = TagFilter(''); final noFilter = TagFilter('');
return _buildFilterRow( return _buildFilterRow(
context: context, context: context,
@ -185,7 +185,7 @@ class CollectionSearchDelegate {
_buildFilterRow( _buildFilterRow(
context: context, context: context,
title: context.l10n.searchSectionRating, title: context.l10n.searchSectionRating,
filters: [0, 5, 4, 3, 2, 1, -1].map((rating) => RatingFilter(rating)).where((f) => containQuery(f.getLabel(context))).toList(), filters: [0, 5, 4, 3, 2, 1, -1].map(RatingFilter.new).where((f) => containQuery(f.getLabel(context))).toList(),
), ),
], ],
); );

View file

@ -0,0 +1,46 @@
import 'package:aves/model/covers.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
enum AppExportItem { covers, favourites, settings }
extension ExtraAppExportItem on AppExportItem {
String getText(BuildContext context) {
switch (this) {
case AppExportItem.covers:
return context.l10n.appExportCovers;
case AppExportItem.favourites:
return context.l10n.appExportFavourites;
case AppExportItem.settings:
return context.l10n.appExportSettings;
}
}
dynamic export(CollectionSource source) {
switch (this) {
case AppExportItem.covers:
return covers.export(source);
case AppExportItem.favourites:
return favourites.export(source);
case AppExportItem.settings:
return settings.export();
}
}
Future<void> import(dynamic jsonMap, CollectionSource source) async {
switch (this) {
case AppExportItem.covers:
covers.import(jsonMap, source);
break;
case AppExportItem.favourites:
favourites.import(jsonMap, source);
break;
case AppExportItem.settings:
await settings.import(jsonMap);
break;
}
}
}

View file

@ -0,0 +1,68 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/settings/app_export/items.dart';
import 'package:flutter/material.dart';
class AppExportItemSelectionDialog extends StatefulWidget {
final String title;
final Set<AppExportItem>? selectableItems, initialSelection;
const AppExportItemSelectionDialog({
Key? key,
required this.title,
this.selectableItems,
this.initialSelection,
}) : super(key: key);
@override
_AppExportItemSelectionDialogState createState() => _AppExportItemSelectionDialogState();
}
class _AppExportItemSelectionDialogState extends State<AppExportItemSelectionDialog> {
final Set<AppExportItem> _selectableItems = {}, _selectedItems = {};
@override
void initState() {
super.initState();
_selectableItems.addAll(widget.selectableItems ?? AppExportItem.values);
_selectedItems.addAll(widget.initialSelection ?? _selectableItems);
}
@override
Widget build(BuildContext context) {
return AvesDialog(
title: widget.title,
scrollableContent: AppExportItem.values.map((v) {
return SwitchListTile(
value: _selectedItems.contains(v),
onChanged: _selectableItems.contains(v)
? (selected) {
if (selected == true) {
_selectedItems.add(v);
} else {
_selectedItems.remove(v);
}
setState(() {});
}
: null,
title: Text(
v.getText(context),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
);
}).toList(),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton(
onPressed: _selectedItems.isEmpty ? null : () => Navigator.pop(context, _selectedItems),
child: Text(context.l10n.applyButtonLabel),
),
],
);
}
}

View file

@ -1,7 +1,7 @@
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
class CrumbLine extends StatefulWidget { class CrumbLine extends StatefulWidget {
final VolumeRelativeDirectory directory; final VolumeRelativeDirectory directory;
@ -42,7 +42,7 @@ class _CrumbLineState extends State<CrumbLine> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<String> parts = [ List<String> parts = [
directory.getVolumeDescription(context), directory.getVolumeDescription(context),
...p.split(directory.relativeDir), ...pContext.split(directory.relativeDir),
]; ];
final crumbStyle = Theme.of(context).textTheme.bodyText2; final crumbStyle = Theme.of(context).textTheme.bodyText2;
final crumbColor = crumbStyle!.color!.withOpacity(.4); final crumbColor = crumbStyle!.color!.withOpacity(.4);
@ -76,7 +76,7 @@ class _CrumbLineState extends State<CrumbLine> {
} }
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
final path = p.joinAll([ final path = pContext.joinAll([
directory.volumePath, directory.volumePath,
...parts.skip(1).take(index), ...parts.skip(1).take(index),
]); ]);

View file

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
@ -14,7 +15,6 @@ import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:path/path.dart' as p;
class FilePicker extends StatefulWidget { class FilePicker extends StatefulWidget {
static const routeName = '/file_picker'; static const routeName = '/file_picker';
@ -31,7 +31,7 @@ class _FilePickerState extends State<FilePicker> {
Set<StorageVolume> get volumes => androidFileUtils.storageVolumes; Set<StorageVolume> get volumes => androidFileUtils.storageVolumes;
String get currentDirectoryPath => p.join(_directory.volumePath, _directory.relativeDir); String get currentDirectoryPath => pContext.join(_directory.volumePath, _directory.relativeDir);
@override @override
void initState() { void initState() {
@ -48,7 +48,7 @@ class _FilePickerState extends State<FilePicker> {
if (showHidden) { if (showHidden) {
return true; return true;
} else { } else {
final isHidden = p.split(v.path).last.startsWith('.'); final isHidden = pContext.split(v.path).last.startsWith('.');
return !isHidden; return !isHidden;
} }
}).toList(); }).toList();
@ -57,7 +57,7 @@ class _FilePickerState extends State<FilePicker> {
if (_directory.relativeDir.isEmpty) { if (_directory.relativeDir.isEmpty) {
return SynchronousFuture(true); return SynchronousFuture(true);
} }
final parent = p.dirname(currentDirectoryPath); final parent = pContext.dirname(currentDirectoryPath);
_goTo(parent); _goTo(parent);
setState(() {}); setState(() {});
return SynchronousFuture(false); return SynchronousFuture(false);
@ -143,7 +143,7 @@ class _FilePickerState extends State<FilePicker> {
if (_directory.relativeDir.isEmpty) { if (_directory.relativeDir.isEmpty) {
return _directory.getVolumeDescription(context); return _directory.getVolumeDescription(context);
} }
return p.split(_directory.relativeDir).last; return pContext.split(_directory.relativeDir).last;
} }
Widget _buildDrawer(BuildContext context) { Widget _buildDrawer(BuildContext context) {
@ -179,7 +179,7 @@ class _FilePickerState extends State<FilePicker> {
Widget _buildContentLine(BuildContext context, FileSystemEntity content) { Widget _buildContentLine(BuildContext context, FileSystemEntity content) {
return ListTile( return ListTile(
leading: const Icon(AIcons.folder), leading: const Icon(AIcons.folder),
title: Text(p.split(content.path).last), title: Text(pContext.split(content.path).last),
onTap: () { onTap: () {
_goTo(content.path); _goTo(content.path);
setState(() {}); setState(() {});
@ -197,7 +197,7 @@ class _FilePickerState extends State<FilePicker> {
contents.add(entity); contents.add(entity);
} }
}, onDone: () { }, onDone: () {
_contents = contents..sort((a, b) => compareAsciiUpperCase(p.split(a.path).last, p.split(b.path).last)); _contents = contents..sort((a, b) => compareAsciiUpperCase(pContext.split(a.path).last, pContext.split(b.path).last));
setState(() {}); setState(() {});
}); });
} }

View file

@ -2,7 +2,7 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:aves/model/actions/settings_actions.dart'; import 'package:aves/model/actions/settings_actions.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
@ -12,12 +12,15 @@ import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/settings/accessibility/accessibility.dart'; import 'package:aves/widgets/settings/accessibility/accessibility.dart';
import 'package:aves/widgets/settings/app_export/items.dart';
import 'package:aves/widgets/settings/app_export/selection_dialog.dart';
import 'package:aves/widgets/settings/language/language.dart'; import 'package:aves/widgets/settings/language/language.dart';
import 'package:aves/widgets/settings/navigation/navigation.dart'; import 'package:aves/widgets/settings/navigation/navigation.dart';
import 'package:aves/widgets/settings/privacy/privacy.dart'; import 'package:aves/widgets/settings/privacy/privacy.dart';
import 'package:aves/widgets/settings/thumbnails/thumbnails.dart'; import 'package:aves/widgets/settings/thumbnails/thumbnails.dart';
import 'package:aves/widgets/settings/video/video.dart'; import 'package:aves/widgets/settings/video/video.dart';
import 'package:aves/widgets/settings/viewer/viewer.dart'; import 'package:aves/widgets/settings/viewer/viewer.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -106,13 +109,32 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
); );
} }
static const String exportVersionKey = 'version';
static const int exportVersion = 1;
void _onActionSelected(SettingsAction action) async { void _onActionSelected(SettingsAction action) async {
final source = context.read<CollectionSource>();
switch (action) { switch (action) {
case SettingsAction.export: case SettingsAction.export:
final toExport = await showDialog<Set<AppExportItem>>(
context: context,
builder: (context) => AppExportItemSelectionDialog(
title: context.l10n.settingsActionExport,
),
);
if (toExport == null || toExport.isEmpty) return;
final allMap = Map.fromEntries(toExport.map((v) {
final jsonMap = v.export(source);
return jsonMap != null ? MapEntry(v.name, jsonMap) : null;
}).whereNotNull());
allMap[exportVersionKey] = exportVersion;
final allJsonString = jsonEncode(allMap);
final success = await storageService.createFile( final success = await storageService.createFile(
'aves-settings-${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}.json', 'aves-settings-${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}.json',
MimeTypes.json, MimeTypes.json,
Uint8List.fromList(utf8.encode(settings.toJson())), Uint8List.fromList(utf8.encode(allJsonString)),
); );
if (success != null) { if (success != null) {
if (success) { if (success) {
@ -128,10 +150,44 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
final bytes = await storageService.openFile(); final bytes = await storageService.openFile();
if (bytes.isNotEmpty) { if (bytes.isNotEmpty) {
try { try {
await settings.fromJson(utf8.decode(bytes)); final allJsonString = utf8.decode(bytes);
final allJsonMap = jsonDecode(allJsonString);
final version = allJsonMap[exportVersionKey];
final importable = <AppExportItem, dynamic>{};
if (version == null) {
// backwards compatibility before versioning
importable[AppExportItem.settings] = allJsonMap;
} else {
if (allJsonMap is! Map) {
debugPrint('failed to import app json=$allJsonMap');
showFeedback(context, context.l10n.genericFailureFeedback);
return;
}
allJsonMap.keys.where((v) => v != exportVersionKey).forEach((k) {
try {
importable[AppExportItem.values.byName(k)] = allJsonMap[k];
} catch (error, stack) {
debugPrint('failed to identify import app item=$k with error=$error\n$stack');
}
});
}
final toImport = await showDialog<Set<AppExportItem>>(
context: context,
builder: (context) => AppExportItemSelectionDialog(
title: context.l10n.settingsActionImport,
selectableItems: importable.keys.toSet(),
),
);
if (toImport == null || toImport.isEmpty) return;
await Future.forEach<AppExportItem>(toImport, (item) async {
return item.import(importable[item], source);
});
showFeedback(context, context.l10n.genericSuccessFeedback); showFeedback(context, context.l10n.genericSuccessFeedback);
} catch (error) { } catch (error) {
debugPrint('failed to import settings, error=$error'); debugPrint('failed to import app json, error=$error');
showFeedback(context, context.l10n.genericFailureFeedback); showFeedback(context, context.l10n.genericFailureFeedback);
} }
} }

View file

@ -133,8 +133,8 @@ class StatsPage extends StatelessWidget {
locationIndicator, locationIndicator,
..._buildFilterSection<String>(context, context.l10n.statsTopCountries, entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)), ..._buildFilterSection<String>(context, context.l10n.statsTopCountries, entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)),
..._buildFilterSection<String>(context, context.l10n.statsTopPlaces, entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)), ..._buildFilterSection<String>(context, context.l10n.statsTopPlaces, entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)),
..._buildFilterSection<String>(context, context.l10n.statsTopTags, entryCountPerTag, (v) => TagFilter(v)), ..._buildFilterSection<String>(context, context.l10n.statsTopTags, entryCountPerTag, TagFilter.new),
if (showRatings) ..._buildFilterSection<int>(context, context.l10n.searchSectionRating, entryCountPerRating, (v) => RatingFilter(v), sortByCount: false, maxRowCount: null), if (showRatings) ..._buildFilterSection<int>(context, context.l10n.searchSectionRating, entryCountPerRating, RatingFilter.new, sortByCount: false, maxRowCount: null),
], ],
); );
} }

View file

@ -96,7 +96,7 @@ class BasicSection extends StatelessWidget {
if (entry.isVideo && !entry.is360) MimeFilter.video, if (entry.isVideo && !entry.is360) MimeFilter.video,
if (album != null) AlbumFilter(album, collection?.source.getAlbumDisplayName(context, album)), if (album != null) AlbumFilter(album, collection?.source.getAlbumDisplayName(context, album)),
if (entry.rating != 0) RatingFilter(entry.rating), if (entry.rating != 0) RatingFilter(entry.rating),
...tags.map((tag) => TagFilter(tag)), ...tags.map(TagFilter.new),
}; };
return AnimatedBuilder( return AnimatedBuilder(
animation: favourites, animation: favourites,

View file

@ -162,6 +162,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
// `enable-accurate-seek`: enable accurate seek // `enable-accurate-seek`: enable accurate seek
// default: 0, in [0, 1] // default: 0, in [0, 1]
// ignore: dead_code
options.setPlayerOption('enable-accurate-seek', accurateSeekEnabled ? 1 : 0); options.setPlayerOption('enable-accurate-seek', accurateSeekEnabled ? 1 : 0);
// `min-frames`: minimal frames to stop pre-reading // `min-frames`: minimal frames to stop pre-reading

View file

@ -5,7 +5,7 @@ version: 1.5.10+64
publish_to: none publish_to: none
environment: environment:
sdk: '>=2.14.0 <3.0.0' sdk: '>=2.15.0 <3.0.0'
# use `scripts/apply_flavor_{flavor}.sh` to set the right dependencies for the flavor # use `scripts/apply_flavor_{flavor}.sh` to set the right dependencies for the flavor
dependencies: dependencies:

View file

@ -52,17 +52,17 @@ void main() {
setUp(() async { setUp(() async {
// specify Posix style path context for consistent behaviour when running tests on Windows // specify Posix style path context for consistent behaviour when running tests on Windows
getIt.registerLazySingleton<p.Context>(() => p.Context(style: p.Style.posix)); getIt.registerLazySingleton<p.Context>(() => p.Context(style: p.Style.posix));
getIt.registerLazySingleton<AvesAvailability>(() => FakeAvesAvailability()); getIt.registerLazySingleton<AvesAvailability>(FakeAvesAvailability.new);
getIt.registerLazySingleton<MetadataDb>(() => FakeMetadataDb()); getIt.registerLazySingleton<MetadataDb>(FakeMetadataDb.new);
getIt.registerLazySingleton<AndroidAppService>(() => FakeAndroidAppService()); getIt.registerLazySingleton<AndroidAppService>(FakeAndroidAppService.new);
getIt.registerLazySingleton<DeviceService>(() => FakeDeviceService()); getIt.registerLazySingleton<DeviceService>(FakeDeviceService.new);
getIt.registerLazySingleton<MediaFileService>(() => FakeMediaFileService()); getIt.registerLazySingleton<MediaFileService>(FakeMediaFileService.new);
getIt.registerLazySingleton<MediaStoreService>(() => FakeMediaStoreService()); getIt.registerLazySingleton<MediaStoreService>(FakeMediaStoreService.new);
getIt.registerLazySingleton<MetadataFetchService>(() => FakeMetadataFetchService()); getIt.registerLazySingleton<MetadataFetchService>(FakeMetadataFetchService.new);
getIt.registerLazySingleton<ReportService>(() => FakeReportService()); getIt.registerLazySingleton<ReportService>(FakeReportService.new);
getIt.registerLazySingleton<StorageService>(() => FakeStorageService()); getIt.registerLazySingleton<StorageService>(FakeStorageService.new);
getIt.registerLazySingleton<WindowService>(() => FakeWindowService()); getIt.registerLazySingleton<WindowService>(FakeWindowService.new);
await settings.init(monitorPlatformSettings: false); await settings.init(monitorPlatformSettings: false);
settings.canUseAnalysisService = false; settings.canUseAnalysisService = false;

View file

@ -11,7 +11,7 @@ void main() {
// specify Posix style path context for consistent behaviour when running tests on Windows // specify Posix style path context for consistent behaviour when running tests on Windows
getIt.registerLazySingleton<p.Context>(() => p.Context(style: p.Style.posix)); getIt.registerLazySingleton<p.Context>(() => p.Context(style: p.Style.posix));
getIt.registerLazySingleton<StorageService>(() => FakeStorageService()); getIt.registerLazySingleton<StorageService>(FakeStorageService.new);
await androidFileUtils.init(); await androidFileUtils.init();
}); });

View file

@ -1,5 +1,6 @@
{ {
"ru": [ "ru": [
"appExportCovers",
"settingsThumbnailShowFavouriteIcon" "settingsThumbnailShowFavouriteIcon"
] ]
} }