Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2022-08-29 21:35:35 +02:00
commit 369fce9991
33 changed files with 145 additions and 67 deletions

View file

@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
## <a id="v1.6.13"></a>[v1.6.13] - 2022-08-29
### Changed
- use natural order when sorting by name items, albums, tags
### Fixed
- adding duplicate items during loading in some cases
- screensaver stopping when device orientation changes
## <a id="v1.6.12"></a>[v1.6.12] - 2022-08-27 ## <a id="v1.6.12"></a>[v1.6.12] - 2022-08-27
### Added ### Added

View file

@ -0,0 +1,5 @@
In v1.6.13:
- play your HEIC motion photos
- find recently downloaded images with the `recently added` filter
- enjoy the app in Dutch
Full changelog available on GitHub

View file

@ -87,6 +87,7 @@
"entryInfoActionEditDate": "Editar data e hora", "entryInfoActionEditDate": "Editar data e hora",
"entryInfoActionEditLocation": "Editar localização", "entryInfoActionEditLocation": "Editar localização",
"entryInfoActionEditDescription": "Editar descrição",
"entryInfoActionEditRating": "Editar classificação", "entryInfoActionEditRating": "Editar classificação",
"entryInfoActionEditTags": "Editar etiquetas", "entryInfoActionEditTags": "Editar etiquetas",
"entryInfoActionRemoveMetadata": "Remover metadados", "entryInfoActionRemoveMetadata": "Remover metadados",
@ -96,6 +97,7 @@
"filterLocationEmptyLabel": "Não localizado", "filterLocationEmptyLabel": "Não localizado",
"filterTagEmptyLabel": "Sem etiqueta", "filterTagEmptyLabel": "Sem etiqueta",
"filterOnThisDayLabel": "Neste dia", "filterOnThisDayLabel": "Neste dia",
"filterRecentlyAddedLabel": "Adicionado recentemente",
"filterRatingUnratedLabel": "Sem classificação", "filterRatingUnratedLabel": "Sem classificação",
"filterRatingRejectedLabel": "Rejeitado", "filterRatingRejectedLabel": "Rejeitado",
"filterTypeAnimatedLabel": "Animado", "filterTypeAnimatedLabel": "Animado",
@ -257,6 +259,8 @@
"locationPickerUseThisLocationButton": "Usar essa localização", "locationPickerUseThisLocationButton": "Usar essa localização",
"editEntryDescriptionDialogTitle": "Descrição",
"editEntryRatingDialogTitle": "Avaliação", "editEntryRatingDialogTitle": "Avaliação",
"removeEntryMetadataDialogTitle": "Remoção de metadados", "removeEntryMetadataDialogTitle": "Remoção de metadados",
@ -451,6 +455,7 @@
"settingsConfirmationDialogDeleteItems": "Pergunte antes de excluir itens para sempre", "settingsConfirmationDialogDeleteItems": "Pergunte antes de excluir itens para sempre",
"settingsConfirmationDialogMoveToBinItems": "Pergunte antes de mover itens para a lixeira", "settingsConfirmationDialogMoveToBinItems": "Pergunte antes de mover itens para a lixeira",
"settingsConfirmationDialogMoveUndatedItems": "Pergunte antes de mover itens sem data de metadados", "settingsConfirmationDialogMoveUndatedItems": "Pergunte antes de mover itens sem data de metadados",
"settingsConfirmationAfterMoveToBinItems": "Mostrar mensagem depois de mover itens para a lixeira",
"settingsNavigationDrawerTile": "Menu de navegação", "settingsNavigationDrawerTile": "Menu de navegação",
"settingsNavigationDrawerEditorTitle": "Menu de navegação", "settingsNavigationDrawerEditorTitle": "Menu de navegação",
@ -479,6 +484,7 @@
"settingsCollectionSelectionQuickActionEditorBanner": "Toque e segure para mover os botões e selecionar quais ações são exibidas ao selecionar itens.", "settingsCollectionSelectionQuickActionEditorBanner": "Toque e segure para mover os botões e selecionar quais ações são exibidas ao selecionar itens.",
"settingsSectionViewer": "Visualizador", "settingsSectionViewer": "Visualizador",
"settingsViewerGestureSideTapNext": "Toque nas bordas da tela para mostrar anterior/seguinte",
"settingsViewerUseCutout": "Usar área de recorte", "settingsViewerUseCutout": "Usar área de recorte",
"settingsViewerMaximumBrightness": "Brilho máximo", "settingsViewerMaximumBrightness": "Brilho máximo",
"settingsMotionPhotoAutoPlay": "Reprodução automática de fotos em movimento", "settingsMotionPhotoAutoPlay": "Reprodução automática de fotos em movimento",

View file

@ -819,7 +819,7 @@ class AvesEntry {
// 1) title ascending // 1) title ascending
// 2) extension ascending // 2) extension ascending
static int compareByName(AvesEntry a, AvesEntry b) { static int compareByName(AvesEntry a, AvesEntry b) {
final c = compareAsciiUpperCase(a.bestTitle ?? '', b.bestTitle ?? ''); final c = compareAsciiUpperCaseNatural(a.bestTitle ?? '', b.bestTitle ?? '');
return c != 0 ? c : compareAsciiUpperCase(a.extension ?? '', b.extension ?? ''); return c != 0 ? c : compareAsciiUpperCase(a.extension ?? '', b.extension ?? '');
} }

View file

@ -118,7 +118,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
int compareTo(CollectionFilter other) { int compareTo(CollectionFilter other) {
final c = displayPriority.compareTo(other.displayPriority); final c = displayPriority.compareTo(other.displayPriority);
// assume we compare context-independent labels // assume we compare context-independent labels
return c != 0 ? c : compareAsciiUpperCase(universalLabel, other.universalLabel); return c != 0 ? c : compareAsciiUpperCaseNatural(universalLabel, other.universalLabel);
} }
} }

View file

@ -19,11 +19,11 @@ mixin AlbumMixin on SourceBase {
int compareAlbumsByName(String a, String b) { int compareAlbumsByName(String a, String b) {
final ua = getAlbumDisplayName(null, a); final ua = getAlbumDisplayName(null, a);
final ub = getAlbumDisplayName(null, b); final ub = getAlbumDisplayName(null, b);
final c = compareAsciiUpperCase(ua, ub); final c = compareAsciiUpperCaseNatural(ua, ub);
if (c != 0) return c; if (c != 0) return c;
final va = androidFileUtils.getStorageVolume(a)?.path ?? ''; final va = androidFileUtils.getStorageVolume(a)?.path ?? '';
final vb = androidFileUtils.getStorageVolume(b)?.path ?? ''; final vb = androidFileUtils.getStorageVolume(b)?.path ?? '';
return compareAsciiUpperCase(va, vb); return compareAsciiUpperCaseNatural(va, vb);
} }
void notifyAlbumsChanged() { void notifyAlbumsChanged() {

View file

@ -40,6 +40,12 @@ mixin SourceBase {
ValueNotifier<SourceState> stateNotifier = ValueNotifier(SourceState.ready); ValueNotifier<SourceState> stateNotifier = ValueNotifier(SourceState.ready);
set state(SourceState value) => stateNotifier.value = value;
SourceState get state => stateNotifier.value;
bool get isReady => state == SourceState.ready;
ValueNotifier<ProgressEvent> progressNotifier = ValueNotifier(const ProgressEvent(done: 0, total: 0)); ValueNotifier<ProgressEvent> progressNotifier = ValueNotifier(const ProgressEvent(done: 0, total: 0));
void setProgress({required int done, required int total}) => progressNotifier.value = ProgressEvent(done: done, total: total); void setProgress({required int done, required int total}) => progressNotifier.value = ProgressEvent(done: done, total: total);
@ -430,7 +436,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
updateDerivedFilters(todoEntries); updateDerivedFilters(todoEntries);
} }
} }
stateNotifier.value = SourceState.ready; state = SourceState.ready;
} }
// monitoring // monitoring

View file

@ -51,7 +51,7 @@ mixin LocationMixin on SourceBase {
final todo = (force ? candidateEntries.where((entry) => entry.hasGps) : candidateEntries.where(locateCountriesTest)).toSet(); final todo = (force ? candidateEntries.where((entry) => entry.hasGps) : candidateEntries.where(locateCountriesTest)).toSet();
if (todo.isEmpty) return; if (todo.isEmpty) return;
stateNotifier.value = SourceState.locatingCountries; state = SourceState.locatingCountries;
var progressDone = 0; var progressDone = 0;
final progressTotal = todo.length; final progressTotal = todo.length;
setProgress(done: progressDone, total: progressTotal); setProgress(done: progressDone, total: progressTotal);
@ -106,7 +106,7 @@ mixin LocationMixin on SourceBase {
knownLocations.putIfAbsent(approximateLatLng(entry), () => entry.addressDetails); knownLocations.putIfAbsent(approximateLatLng(entry), () => entry.addressDetails);
}); });
stateNotifier.value = SourceState.locatingPlaces; state = SourceState.locatingPlaces;
var progressDone = 0; var progressDone = 0;
final progressTotal = todo.length; final progressTotal = todo.length;
setProgress(done: progressDone, total: progressTotal); setProgress(done: progressDone, total: progressTotal);

View file

@ -42,7 +42,7 @@ class MediaStoreSource extends CollectionSource {
Future<void> _loadEssentials() async { Future<void> _loadEssentials() async {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
stateNotifier.value = SourceState.loading; state = SourceState.loading;
await metadataDb.init(); await metadataDb.init();
await favourites.init(); await favourites.init();
await covers.init(); await covers.init();
@ -69,7 +69,7 @@ class MediaStoreSource extends CollectionSource {
}) async { }) async {
debugPrint('$runtimeType refresh start'); debugPrint('$runtimeType refresh start');
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
stateNotifier.value = SourceState.loading; state = SourceState.loading;
clearEntries(); clearEntries();
final Set<AvesEntry> topEntries = {}; final Set<AvesEntry> topEntries = {};
@ -195,7 +195,7 @@ class MediaStoreSource extends CollectionSource {
if (canAnalyze) { if (canAnalyze) {
await analyze(analysisController, entries: analysisEntries); await analyze(analysisController, entries: analysisEntries);
} else { } else {
stateNotifier.value = SourceState.ready; state = SourceState.ready;
} }
// the home page may not reflect the current derived filters // the home page may not reflect the current derived filters
@ -216,7 +216,7 @@ class MediaStoreSource extends CollectionSource {
// sometimes yields an entry with its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg` // sometimes yields an entry with its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg`
@override @override
Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController}) async { Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController}) async {
if (_initState == SourceInitializationState.none || !isMonitoring) return changedUris; if (_initState == SourceInitializationState.none || !isMonitoring || !isReady) return changedUris;
debugPrint('$runtimeType refreshUris ${changedUris.length} uris'); debugPrint('$runtimeType refreshUris ${changedUris.length} uris');
final uriByContentId = Map.fromEntries(changedUris.map((uri) { final uriByContentId = Map.fromEntries(changedUris.map((uri) {

View file

@ -31,7 +31,7 @@ mixin TagMixin on SourceBase {
final todo = force ? candidateEntries : candidateEntries.where(catalogEntriesTest).toSet(); final todo = force ? candidateEntries : candidateEntries.where(catalogEntriesTest).toSet();
if (todo.isEmpty) return; if (todo.isEmpty) return;
stateNotifier.value = SourceState.cataloguing; state = SourceState.cataloguing;
var progressDone = 0; var progressDone = 0;
final progressTotal = todo.length; final progressTotal = todo.length;
setProgress(done: progressDone, total: progressTotal); setProgress(done: progressDone, total: progressTotal);
@ -64,7 +64,7 @@ mixin TagMixin on SourceBase {
} }
void updateTags() { void updateTags() {
final updatedTags = visibleEntries.expand((entry) => entry.tags).toSet().toList()..sort(compareAsciiUpperCase); final updatedTags = visibleEntries.expand((entry) => entry.tags).toSet().toList()..sort(compareAsciiUpperCaseNatural);
if (!listEquals(updatedTags, sortedTags)) { if (!listEquals(updatedTags, sortedTags)) {
sortedTags = List.unmodifiable(updatedTags); sortedTags = List.unmodifiable(updatedTags);
invalidateTagFilterSummary(); invalidateTagFilterSummary();

View file

@ -85,7 +85,7 @@ class Analyzer {
bool get isRunning => serviceState == AnalyzerState.running; bool get isRunning => serviceState == AnalyzerState.running;
SourceState get sourceState => _source.stateNotifier.value; SourceState get sourceState => _source.state;
static const notificationUpdateInterval = Duration(seconds: 1); static const notificationUpdateInterval = Duration(seconds: 1);
@ -151,7 +151,7 @@ class Analyzer {
} }
void _onSourceStateChanged() { void _onSourceStateChanged() {
if (sourceState == SourceState.ready) { if (_source.isReady) {
_refreshApp(); _refreshApp();
_serviceStateNotifier.value = AnalyzerState.stopping; _serviceStateNotifier.value = AnalyzerState.stopping;
} }

View file

@ -4,7 +4,6 @@ import 'package:aves/app_flavor.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/model/source/media_store_source.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/home_widget.dart'; import 'package:aves/widgets/home_widget.dart';
@ -64,7 +63,7 @@ Future<AvesEntry?> _getWidgetEntry(int widgetId, bool reuseEntry) async {
final source = MediaStoreSource(); final source = MediaStoreSource();
final readyCompleter = Completer(); final readyCompleter = Completer();
source.stateNotifier.addListener(() { source.stateNotifier.addListener(() {
if (source.stateNotifier.value == SourceState.ready) { if (source.isReady) {
readyCompleter.complete(); readyCompleter.complete();
} }
}); });

View file

@ -60,7 +60,7 @@ class SourceStateSubtitle extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sourceState = source.stateNotifier.value; final sourceState = source.state;
final subtitle = sourceState.getName(context.l10n); final subtitle = sourceState.getName(context.l10n);
if (subtitle == null) return const SizedBox(); if (subtitle == null) return const SizedBox();

View file

@ -192,7 +192,7 @@ class _TagEditorPageState extends State<TagEditorPage> {
return entryCountByTag.entries.toList() return entryCountByTag.entries.toList()
..sort((kv1, kv2) { ..sort((kv1, kv2) {
final c = kv2.value.compareTo(kv1.value); final c = kv2.value.compareTo(kv1.value);
return c != 0 ? c : compareAsciiUpperCase(kv1.key, kv2.key); return c != 0 ? c : compareAsciiUpperCaseNatural(kv1.key, kv2.key);
}); });
} }

View file

@ -48,7 +48,7 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
final volumeTiles = <Widget>[]; final volumeTiles = <Widget>[];
if (_allVolumes.length > 1) { if (_allVolumes.length > 1) {
final byPrimary = groupBy<StorageVolume, bool>(_allVolumes, (volume) => volume.isPrimary); final byPrimary = groupBy<StorageVolume, bool>(_allVolumes, (volume) => volume.isPrimary);
int compare(StorageVolume a, StorageVolume b) => compareAsciiUpperCase(a.path, b.path); int compare(StorageVolume a, StorageVolume b) => compareAsciiUpperCaseNatural(a.path, b.path);
final primaryVolumes = (byPrimary[true] ?? [])..sort(compare); final primaryVolumes = (byPrimary[true] ?? [])..sort(compare);
final otherVolumes = (byPrimary[false] ?? [])..sort(compare); final otherVolumes = (byPrimary[false] ?? [])..sort(compare);
volumeTiles.addAll([ volumeTiles.addAll([

View file

@ -266,7 +266,7 @@ class _HomePageState extends State<HomePage> {
// wait for collection to pass the `loading` state // wait for collection to pass the `loading` state
final completer = Completer(); final completer = Completer();
void _onSourceStateChanged() { void _onSourceStateChanged() {
if (source.stateNotifier.value != SourceState.loading) { if (source.state != SourceState.loading) {
source.stateNotifier.removeListener(_onSourceStateChanged); source.stateNotifier.removeListener(_onSourceStateChanged);
completer.complete(); completer.complete();
} }

View file

@ -200,7 +200,7 @@ class _FilePickerState extends State<FilePicker> {
contents.add(entity); contents.add(entity);
} }
}, onDone: () { }, onDone: () {
_contents = contents..sort((a, b) => compareAsciiUpperCase(pContext.split(a.path).last, pContext.split(b.path).last)); _contents = contents..sort((a, b) => compareAsciiUpperCaseNatural(pContext.split(a.path).last, pContext.split(b.path).last));
setState(() {}); setState(() {});
}); });
} }

View file

@ -58,7 +58,7 @@ class BasicSection extends StatelessWidget {
} }
Widget _buildChips(BuildContext context) { Widget _buildChips(BuildContext context) {
final tags = entry.tags.toList()..sort(compareAsciiUpperCase); final tags = entry.tags.toList()..sort(compareAsciiUpperCaseNatural);
final album = entry.directory; final album = entry.directory;
final filters = { final filters = {
MimeFilter(entry.mimeType), MimeFilter(entry.mimeType),

View file

@ -4,7 +4,6 @@ import 'package:aves/model/settings/enums/slideshow_interval.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/identity/empty.dart';
@ -29,7 +28,7 @@ class ScreenSaverPage extends StatefulWidget {
State<ScreenSaverPage> createState() => _ScreenSaverPageState(); State<ScreenSaverPage> createState() => _ScreenSaverPageState();
} }
class _ScreenSaverPageState extends State<ScreenSaverPage> { class _ScreenSaverPageState extends State<ScreenSaverPage> with WidgetsBindingObserver {
late final ViewerController _viewerController; late final ViewerController _viewerController;
CollectionLens? _slideshowCollection; CollectionLens? _slideshowCollection;
@ -47,24 +46,24 @@ class _ScreenSaverPageState extends State<ScreenSaverPage> {
); );
source.stateNotifier.addListener(_onSourceStateChanged); source.stateNotifier.addListener(_onSourceStateChanged);
_initSlideshowCollection(); _initSlideshowCollection();
} WidgetsBinding.instance.addObserver(this);
void _onSourceStateChanged() {
if (_slideshowCollection == null) {
_initSlideshowCollection();
if (_slideshowCollection != null) {
setState(() {});
}
}
} }
@override @override
void dispose() { void dispose() {
source.stateNotifier.removeListener(_onSourceStateChanged); source.stateNotifier.removeListener(_onSourceStateChanged);
_viewerController.dispose(); _viewerController.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_viewerController.autopilot = true;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child; Widget child;
@ -102,8 +101,17 @@ class _ScreenSaverPageState extends State<ScreenSaverPage> {
); );
} }
void _onSourceStateChanged() {
if (_slideshowCollection == null) {
_initSlideshowCollection();
if (_slideshowCollection != null) {
setState(() {});
}
}
}
void _initSlideshowCollection() { void _initSlideshowCollection() {
if (source.stateNotifier.value != SourceState.ready || _slideshowCollection != null) return; if (!source.isReady || _slideshowCollection != null) return;
final originalCollection = CollectionLens( final originalCollection = CollectionLens(
source: source, source: source,

View file

@ -6,7 +6,7 @@ repository: https://github.com/deckerst/aves
# - github changelog: /CHANGELOG.md # - github changelog: /CHANGELOG.md
# - play changelog: /whatsnew/whatsnew-en-US # - play changelog: /whatsnew/whatsnew-en-US
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/1XXX.txt # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/1XXX.txt
version: 1.6.12+78 version: 1.6.13+79
publish_to: none publish_to: none
environment: environment:

View file

@ -1,7 +1,7 @@
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_app_service.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:test/fake.dart';
class FakeAndroidAppService extends Fake implements AndroidAppService { class FakeAndroidAppService extends Fake implements AndroidAppService {
@override @override

View file

@ -1,6 +1,6 @@
import 'package:aves/model/availability.dart'; import 'package:aves/model/availability.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:test/fake.dart';
class FakeAvesAvailability extends Fake implements AvesAvailability { class FakeAvesAvailability extends Fake implements AvesAvailability {
@override @override

View file

@ -1,6 +1,6 @@
import 'package:aves/services/device_service.dart'; import 'package:aves/services/device_service.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:test/fake.dart';
class FakeDeviceService extends Fake implements DeviceService { class FakeDeviceService extends Fake implements DeviceService {
@override @override

View file

@ -0,0 +1,13 @@
import 'package:aves/model/entry.dart';
import 'package:aves/services/media/media_fetch_service.dart';
import 'package:collection/collection.dart';
import 'package:test/fake.dart';
class FakeMediaFetchService extends Fake implements MediaFetchService {
Set<AvesEntry> entries = {};
@override
Future<AvesEntry?> getEntry(String uri, String? mimeType) async {
return entries.firstWhereOrNull((v) => v.uri == uri);
}
}

View file

@ -2,17 +2,28 @@ import 'package:aves/model/entry.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/image_op_events.dart';
import 'package:aves/services/media/media_store_service.dart'; import 'package:aves/services/media/media_store_service.dart';
import 'package:flutter/foundation.dart'; import 'package:test/fake.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeMediaStoreService extends Fake implements MediaStoreService { class FakeMediaStoreService extends Fake implements MediaStoreService {
Set<AvesEntry> entries = {}; late Set<AvesEntry> entries;
Duration? latency;
void reset() {
entries = {};
latency = null;
}
@override @override
Future<List<int>> checkObsoleteContentIds(List<int?> knownContentIds) => SynchronousFuture([]); Future<List<int>> checkObsoleteContentIds(List<int?> knownContentIds) async {
if (latency != null) await Future.delayed(latency!);
return [];
}
@override @override
Future<List<int>> checkObsoletePaths(Map<int?, String?> knownPathById) => SynchronousFuture([]); Future<List<int>> checkObsoletePaths(Map<int?, String?> knownPathById) async {
if (latency != null) await Future.delayed(latency!);
return [];
}
@override @override
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) => Stream.fromIterable(entries); Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) => Stream.fromIterable(entries);
@ -23,14 +34,15 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
static int get dateSecs => DateTime.now().millisecondsSinceEpoch ~/ 1000; static int get dateSecs => DateTime.now().millisecondsSinceEpoch ~/ 1000;
static AvesEntry newImage(String album, String filenameWithoutExtension) { static AvesEntry newImage(String album, String filenameWithoutExtension, {int? id, int? contentId}) {
final id = nextId; id ??= nextId;
contentId ??= id;
final date = dateSecs; final date = dateSecs;
return AvesEntry( return AvesEntry(
id: id, id: id,
uri: 'content://media/external/images/media/$id', uri: 'content://media/external/images/media/$contentId',
path: '$album/$filenameWithoutExtension.jpg', path: '$album/$filenameWithoutExtension.jpg',
contentId: id, contentId: contentId,
pageId: null, pageId: null,
sourceMimeType: MimeTypes.jpeg, sourceMimeType: MimeTypes.jpeg,
width: 360, width: 360,

View file

@ -7,7 +7,7 @@ import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.dart'; import 'package:aves/model/metadata/trash.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:test/fake.dart';
class FakeMetadataDb extends Fake implements MetadataDb { class FakeMetadataDb extends Fake implements MetadataDb {
static int _lastId = 0; static int _lastId = 0;

View file

@ -2,7 +2,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/services/metadata/metadata_fetch_service.dart'; import 'package:aves/services/metadata/metadata_fetch_service.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:test/fake.dart';
class FakeMetadataFetchService extends Fake implements MetadataFetchService { class FakeMetadataFetchService extends Fake implements MetadataFetchService {
final Map<AvesEntry, CatalogMetadata> _metaMap = {}; final Map<AvesEntry, CatalogMetadata> _metaMap = {};

View file

@ -1,6 +1,5 @@
import 'package:aves_report/aves_report.dart'; import 'package:aves_report/aves_report.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeReportService extends ReportService { class FakeReportService extends ReportService {
@override @override

View file

@ -1,7 +1,7 @@
import 'package:aves/services/storage_service.dart'; import 'package:aves/services/storage_service.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:test/fake.dart';
class FakeStorageService extends Fake implements StorageService { class FakeStorageService extends Fake implements StorageService {
static const primaryRootAlbum = '/storage/emulated/0'; static const primaryRootAlbum = '/storage/emulated/0';

View file

@ -1,7 +1,7 @@
import 'package:aves/services/window_service.dart'; import 'package:aves/services/window_service.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:test/fake.dart';
class FakeWindowService extends Fake implements WindowService { class FakeWindowService extends Fake implements WindowService {
@override @override

View file

@ -10,11 +10,12 @@ import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/model/source/media_store_source.dart';
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/services/device_service.dart'; import 'package:aves/services/device_service.dart';
import 'package:aves/services/media/media_fetch_service.dart';
import 'package:aves/services/media/media_store_service.dart'; import 'package:aves/services/media/media_store_service.dart';
import 'package:aves/services/metadata/metadata_fetch_service.dart'; import 'package:aves/services/metadata/metadata_fetch_service.dart';
import 'package:aves/services/storage_service.dart'; import 'package:aves/services/storage_service.dart';
@ -29,6 +30,7 @@ import 'package:path/path.dart' as p;
import '../fake/android_app_service.dart'; import '../fake/android_app_service.dart';
import '../fake/availability.dart'; import '../fake/availability.dart';
import '../fake/device_service.dart'; import '../fake/device_service.dart';
import '../fake/media_fetch_service.dart';
import '../fake/media_store_service.dart'; import '../fake/media_store_service.dart';
import '../fake/metadata_db.dart'; import '../fake/metadata_db.dart';
import '../fake/metadata_fetch_service.dart'; import '../fake/metadata_fetch_service.dart';
@ -49,7 +51,7 @@ void main() {
countryName: 'AUS', countryName: 'AUS',
); );
setUp(() async { setUpAll(() 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.new); getIt.registerLazySingleton<AvesAvailability>(FakeAvesAvailability.new);
@ -57,6 +59,7 @@ void main() {
getIt.registerLazySingleton<AndroidAppService>(FakeAndroidAppService.new); getIt.registerLazySingleton<AndroidAppService>(FakeAndroidAppService.new);
getIt.registerLazySingleton<DeviceService>(FakeDeviceService.new); getIt.registerLazySingleton<DeviceService>(FakeDeviceService.new);
getIt.registerLazySingleton<MediaFetchService>(FakeMediaFetchService.new);
getIt.registerLazySingleton<MediaStoreService>(FakeMediaStoreService.new); getIt.registerLazySingleton<MediaStoreService>(FakeMediaStoreService.new);
getIt.registerLazySingleton<MetadataFetchService>(FakeMetadataFetchService.new); getIt.registerLazySingleton<MetadataFetchService>(FakeMetadataFetchService.new);
getIt.registerLazySingleton<ReportService>(FakeReportService.new); getIt.registerLazySingleton<ReportService>(FakeReportService.new);
@ -65,9 +68,14 @@ void main() {
await settings.init(monitorPlatformSettings: false); await settings.init(monitorPlatformSettings: false);
settings.canUseAnalysisService = false; settings.canUseAnalysisService = false;
await androidFileUtils.init();
}); });
tearDown(() async { setUp(() async {
(getIt<MediaStoreService>() as FakeMediaStoreService).reset();
});
tearDownAll(() async {
await getIt.reset(); await getIt.reset();
}); });
@ -75,7 +83,7 @@ void main() {
final source = MediaStoreSource(); final source = MediaStoreSource();
final readyCompleter = Completer(); final readyCompleter = Completer();
source.stateNotifier.addListener(() { source.stateNotifier.addListener(() {
if (source.stateNotifier.value == SourceState.ready) { if (source.isReady) {
readyCompleter.complete(); readyCompleter.complete();
} }
}); });
@ -84,6 +92,26 @@ void main() {
return source; return source;
} }
test('initial load v. refresh race condition', () async {
const latency = Duration(milliseconds: 100);
final loadEntry = FakeMediaStoreService.newImage(testAlbum, 'image1', id: -1, contentId: 1);
final refreshEntry = FakeMediaStoreService.newImage(testAlbum, 'image1', id: -1, contentId: 1);
(mediaStoreService as FakeMediaStoreService)
..entries = {loadEntry}
..latency = latency;
(mediaFetchService as FakeMediaFetchService).entries = {refreshEntry};
final source = MediaStoreSource();
unawaited(source.init());
await Future.delayed(const Duration(milliseconds: 10));
expect(source.initState, SourceInitializationState.full);
await source.refreshUris({refreshEntry.uri});
await Future.delayed(const Duration(seconds: 1));
expect(source.allEntries.length, 1);
});
test('album/country/tag hidden on launch when their items are hidden by entry prop', () async { test('album/country/tag hidden on launch when their items are hidden by entry prop', () async {
settings.hiddenFilters = {const AlbumFilter(testAlbum, 'whatever')}; settings.hiddenFilters = {const AlbumFilter(testAlbum, 'whatever')};
@ -336,7 +364,6 @@ void main() {
FakeMediaStoreService.newImage('${FakeStorageService.primaryPath}Pictures/Arendt', '1'), FakeMediaStoreService.newImage('${FakeStorageService.primaryPath}Pictures/Arendt', '1'),
}; };
await androidFileUtils.init();
final source = await _initSource(); final source = await _initSource();
await tester.pumpWidget( await tester.pumpWidget(
Builder( Builder(

View file

@ -22,14 +22,6 @@
"settingsViewerGestureSideTapNext" "settingsViewerGestureSideTapNext"
], ],
"pt": [
"entryInfoActionEditDescription",
"filterRecentlyAddedLabel",
"editEntryDescriptionDialogTitle",
"settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext"
],
"ru": [ "ru": [
"entryInfoActionEditDescription", "entryInfoActionEditDescription",
"filterOnThisDayLabel", "filterOnThisDayLabel",

View file

@ -1,4 +1,4 @@
In v1.6.12: In v1.6.13:
- play your HEIC motion photos - play your HEIC motion photos
- find recently downloaded images with the `recently added` filter - find recently downloaded images with the `recently added` filter
- enjoy the app in Dutch - enjoy the app in Dutch