Merge branch 'develop'
This commit is contained in:
commit
369fce9991
33 changed files with 145 additions and 67 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -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
|
||||||
|
|
5
fastlane/metadata/android/en-US/changelogs/1079.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/1079.txt
Normal 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
|
|
@ -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",
|
||||||
|
|
|
@ -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 ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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([
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(() {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
13
test/fake/media_fetch_service.dart
Normal file
13
test/fake/media_fetch_service.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 = {};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -22,14 +22,6 @@
|
||||||
"settingsViewerGestureSideTapNext"
|
"settingsViewerGestureSideTapNext"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
|
||||||
"entryInfoActionEditDescription",
|
|
||||||
"filterRecentlyAddedLabel",
|
|
||||||
"editEntryDescriptionDialogTitle",
|
|
||||||
"settingsConfirmationAfterMoveToBinItems",
|
|
||||||
"settingsViewerGestureSideTapNext"
|
|
||||||
],
|
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
"entryInfoActionEditDescription",
|
"entryInfoActionEditDescription",
|
||||||
"filterOnThisDayLabel",
|
"filterOnThisDayLabel",
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue