#533 places: page, navigation

This commit is contained in:
Thibault Deckers 2023-02-22 19:49:19 +01:00
parent 300ce4f1bd
commit af6fd8f11b
27 changed files with 505 additions and 91 deletions

View file

@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
### Added
- Places: page & navigation entry
### Fixed
- replacing when moving item to vault
- exporting item to vault
## <a id="v1.8.1"></a>[v1.8.1] - 2023-02-21
### Added

View file

@ -74,6 +74,7 @@
"chipActionDelete": "Delete",
"chipActionGoToAlbumPage": "Show in Albums",
"chipActionGoToCountryPage": "Show in Countries",
"chipActionGoToPlacePage": "Show in Places",
"chipActionGoToTagPage": "Show in Tags",
"chipActionFilterOut": "Filter out",
"chipActionFilterIn": "Filter in",
@ -621,6 +622,7 @@
"drawerCollectionSphericalVideos": "360° Videos",
"drawerAlbumPage": "Albums",
"drawerCountryPage": "Countries",
"drawerPlacePage": "Places",
"drawerTagPage": "Tags",
"sortByDate": "By date",
@ -665,6 +667,9 @@
"countryPageTitle": "Countries",
"countryEmpty": "No countries",
"placePageTitle": "Places",
"placeEmpty": "No places",
"tagPageTitle": "Tags",
"tagEmpty": "No tags",

View file

@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart';
enum ChipAction {
goToAlbumPage,
goToCountryPage,
goToPlacePage,
goToTagPage,
reverse,
hide,
@ -18,6 +19,8 @@ extension ExtraChipAction on ChipAction {
return context.l10n.chipActionGoToAlbumPage;
case ChipAction.goToCountryPage:
return context.l10n.chipActionGoToCountryPage;
case ChipAction.goToPlacePage:
return context.l10n.chipActionGoToPlacePage;
case ChipAction.goToTagPage:
return context.l10n.chipActionGoToTagPage;
case ChipAction.reverse:
@ -37,7 +40,9 @@ extension ExtraChipAction on ChipAction {
case ChipAction.goToAlbumPage:
return AIcons.album;
case ChipAction.goToCountryPage:
return AIcons.location;
return AIcons.country;
case ChipAction.goToPlacePage:
return AIcons.place;
case ChipAction.goToTagPage:
return AIcons.tag;
case ChipAction.reverse:

View file

@ -51,6 +51,8 @@ class LocationFilter extends CoveredCollectionFilter {
String? get countryCode => _countryCode;
String get place => _location;
@override
EntryFilter get positiveTest => _test;
@ -65,17 +67,25 @@ class LocationFilter extends CoveredCollectionFilter {
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
if (_countryCode != null && device.canRenderFlagEmojis) {
final flag = countryCodeToFlag(_countryCode);
if (flag != null) {
return Text(
flag,
style: TextStyle(fontSize: size),
textScaleFactor: 1.0,
);
}
if (_location.isEmpty) {
return Icon(AIcons.locationUnlocated, size: size);
}
switch (level) {
case LocationLevel.place:
return Icon(AIcons.place, size: size);
case LocationLevel.country:
if (_countryCode != null && device.canRenderFlagEmojis) {
final flag = countryCodeToFlag(_countryCode);
if (flag != null) {
return Text(
flag,
style: TextStyle(fontSize: size),
textScaleFactor: 1.0,
);
}
}
return Icon(AIcons.country, size: size);
}
return Icon(_location.isEmpty ? AIcons.locationUnlocated : AIcons.location, size: size);
}
@override

View file

@ -23,8 +23,10 @@ class PlaceholderFilter extends CollectionFilter {
PlaceholderFilter._private(this.placeholder) : super(reversed: false) {
switch (placeholder) {
case _country:
_icon = AIcons.country;
break;
case _place:
_icon = AIcons.location;
_icon = AIcons.place;
break;
}
}

View file

@ -6,6 +6,7 @@ import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/places_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:flutter/material.dart';
@ -40,6 +41,7 @@ class SettingsDefaults {
static const drawerPageBookmarks = [
AlbumListPage.routeName,
CountryListPage.routeName,
PlaceListPage.routeName,
TagListPage.routeName,
];
@ -65,6 +67,7 @@ class SettingsDefaults {
static const albumGroupFactor = AlbumChipGroupFactor.importance;
static const albumSortFactor = ChipSortFactor.name;
static const countrySortFactor = ChipSortFactor.name;
static const placeSortFactor = ChipSortFactor.name;
static const tagSortFactor = ChipSortFactor.name;
// viewer

View file

@ -18,10 +18,9 @@ extension ExtraThumbnailOverlayLocationIcon on ThumbnailOverlayLocationIcon {
IconData getIcon(BuildContext context) {
switch (this) {
case ThumbnailOverlayLocationIcon.located:
return AIcons.location;
case ThumbnailOverlayLocationIcon.unlocated:
return AIcons.locationUnlocated;
case ThumbnailOverlayLocationIcon.located:
case ThumbnailOverlayLocationIcon.none:
return AIcons.location;
}

View file

@ -20,6 +20,7 @@ import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/search/page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/places_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves_map/aves_map.dart';
import 'package:collection/collection.dart';
@ -106,9 +107,11 @@ class Settings extends ChangeNotifier {
static const albumGroupFactorKey = 'album_group_factor';
static const albumSortFactorKey = 'album_sort_factor';
static const countrySortFactorKey = 'country_sort_factor';
static const placeSortFactorKey = 'place_sort_factor';
static const tagSortFactorKey = 'tag_sort_factor';
static const albumSortReverseKey = 'album_sort_reverse';
static const countrySortReverseKey = 'country_sort_reverse';
static const placeSortReverseKey = 'place_sort_reverse';
static const tagSortReverseKey = 'tag_sort_reverse';
static const pinnedFiltersKey = 'pinned_filters';
static const hiddenFiltersKey = 'hidden_filters';
@ -265,6 +268,7 @@ class Settings extends ChangeNotifier {
drawerPageBookmarks = [
AlbumListPage.routeName,
CountryListPage.routeName,
PlaceListPage.routeName,
TagListPage.routeName,
SearchPage.routeName,
];
@ -543,6 +547,10 @@ class Settings extends ChangeNotifier {
set countrySortFactor(ChipSortFactor newValue) => _set(countrySortFactorKey, newValue.toString());
ChipSortFactor get placeSortFactor => getEnumOrDefault(placeSortFactorKey, SettingsDefaults.placeSortFactor, ChipSortFactor.values);
set placeSortFactor(ChipSortFactor newValue) => _set(placeSortFactorKey, newValue.toString());
ChipSortFactor get tagSortFactor => getEnumOrDefault(tagSortFactorKey, SettingsDefaults.tagSortFactor, ChipSortFactor.values);
set tagSortFactor(ChipSortFactor newValue) => _set(tagSortFactorKey, newValue.toString());
@ -555,6 +563,10 @@ class Settings extends ChangeNotifier {
set countrySortReverse(bool newValue) => _set(countrySortReverseKey, newValue);
bool get placeSortReverse => getBool(placeSortReverseKey) ?? false;
set placeSortReverse(bool newValue) => _set(placeSortReverseKey, newValue);
bool get tagSortReverse => getBool(tagSortReverseKey) ?? false;
set tagSortReverse(bool newValue) => _set(tagSortReverseKey, newValue);
@ -1038,6 +1050,7 @@ class Settings extends ChangeNotifier {
case showThumbnailVideoDurationKey:
case albumSortReverseKey:
case countrySortReverseKey:
case placeSortReverseKey:
case tagSortReverseKey:
case showOverlayOnOpeningKey:
case showOverlayMinimapKey:
@ -1084,6 +1097,7 @@ class Settings extends ChangeNotifier {
case albumGroupFactorKey:
case albumSortFactorKey:
case countrySortFactorKey:
case placeSortFactorKey:
case tagSortFactorKey:
case imageBackgroundKey:
case videoAutoPlayModeKey:

View file

@ -14,7 +14,7 @@ import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/events.dart';
import 'package:aves/model/source/location.dart';
import 'package:aves/model/source/location/location.dart';
import 'package:aves/model/source/section_keys.dart';
import 'package:aves/model/source/tag.dart';
import 'package:aves/utils/change_notifier.dart';

View file

@ -15,7 +15,9 @@ import 'package:aves/model/source/album.dart';
import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/events.dart';
import 'package:aves/model/source/location.dart';
import 'package:aves/model/source/location/country.dart';
import 'package:aves/model/source/location/location.dart';
import 'package:aves/model/source/location/place.dart';
import 'package:aves/model/source/tag.dart';
import 'package:aves/model/source/trash.dart';
import 'package:aves/model/vaults/vaults.dart';
@ -54,7 +56,7 @@ mixin SourceBase {
void invalidateEntries();
}
abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin, TrashMixin {
abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, PlaceMixin, LocationMixin, TagMixin, TrashMixin {
CollectionSource() {
settings.updateStream.where((event) => event.key == Settings.localeKey).listen((_) => invalidateAlbumDisplayNames());
settings.updateStream.where((event) => event.key == Settings.hiddenFiltersKey).listen((event) {
@ -136,6 +138,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
invalidateEntries();
invalidateAlbumFilterSummary(entries: entries, notify: notify);
invalidateCountryFilterSummary(entries: entries, notify: notify);
invalidatePlaceFilterSummary(entries: entries, notify: notify);
invalidateTagFilterSummary(entries: entries, notify: notify);
}
@ -501,21 +504,42 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
int count(CollectionFilter filter) {
if (filter is AlbumFilter) return albumEntryCount(filter);
if (filter is LocationFilter) return countryEntryCount(filter);
if (filter is LocationFilter) {
switch (filter.level) {
case LocationLevel.country:
return countryEntryCount(filter);
case LocationLevel.place:
return placeEntryCount(filter);
}
}
if (filter is TagFilter) return tagEntryCount(filter);
return 0;
}
int size(CollectionFilter filter) {
if (filter is AlbumFilter) return albumSize(filter);
if (filter is LocationFilter) return countrySize(filter);
if (filter is LocationFilter) {
switch (filter.level) {
case LocationLevel.country:
return countrySize(filter);
case LocationLevel.place:
return placeSize(filter);
}
}
if (filter is TagFilter) return tagSize(filter);
return 0;
}
AvesEntry? recentEntry(CollectionFilter filter) {
if (filter is AlbumFilter) return albumRecentEntry(filter);
if (filter is LocationFilter) return countryRecentEntry(filter);
if (filter is LocationFilter) {
switch (filter.level) {
case LocationLevel.country:
return countryRecentEntry(filter);
case LocationLevel.place:
return placeRecentEntry(filter);
}
}
if (filter is TagFilter) return tagRecentEntry(filter);
return null;
}

View file

@ -0,0 +1,66 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/utils/collection_utils.dart';
import 'package:collection/collection.dart';
mixin CountryMixin on SourceBase {
// filter summary
// by country code
final Map<String, int> _filterEntryCountMap = {}, _filterSizeMap = {};
final Map<String, AvesEntry?> _filterRecentEntryMap = {};
void invalidateCountryFilterSummary({
Set<AvesEntry>? entries,
Set<String>? countryCodes,
bool notify = true,
}) {
if (_filterEntryCountMap.isEmpty && _filterSizeMap.isEmpty && _filterRecentEntryMap.isEmpty) return;
if (entries == null && countryCodes == null) {
_filterEntryCountMap.clear();
_filterSizeMap.clear();
_filterRecentEntryMap.clear();
} else {
countryCodes ??= {};
if (entries != null) {
countryCodes.addAll(entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.countryCode).whereNotNull());
}
countryCodes.forEach((countryCode) {
_filterEntryCountMap.remove(countryCode);
_filterSizeMap.remove(countryCode);
_filterRecentEntryMap.remove(countryCode);
});
}
if (notify) {
eventBus.fire(CountrySummaryInvalidatedEvent(countryCodes));
}
}
int countryEntryCount(LocationFilter filter) {
final countryCode = filter.countryCode;
if (countryCode == null) return 0;
return _filterEntryCountMap.putIfAbsent(countryCode, () => visibleEntries.where(filter.test).length);
}
int countrySize(LocationFilter filter) {
final countryCode = filter.countryCode;
if (countryCode == null) return 0;
return _filterSizeMap.putIfAbsent(countryCode, () => visibleEntries.where(filter.test).map((v) => v.sizeBytes).sum);
}
AvesEntry? countryRecentEntry(LocationFilter filter) {
final countryCode = filter.countryCode;
if (countryCode == null) return null;
return _filterRecentEntryMap.putIfAbsent(countryCode, () => sortedEntriesByDate.firstWhereOrNull(filter.test));
}
}
class CountriesChangedEvent {}
class CountrySummaryInvalidatedEvent {
final Set<String>? countryCodes;
const CountrySummaryInvalidatedEvent(this.countryCodes);
}

View file

@ -6,15 +6,15 @@ import 'package:aves/model/filters/location.dart';
import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/location/country.dart';
import 'package:aves/model/source/location/place.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/collection_utils.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:tuple/tuple.dart';
mixin LocationMixin on SourceBase {
mixin LocationMixin on CountryMixin, PlaceMixin {
static const commitCountThreshold = 200;
static const _stopCheckCountThreshold = 50;
@ -150,7 +150,7 @@ mixin LocationMixin on SourceBase {
}
void updateLocations() {
final locations = visibleEntries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails).whereNotNull().toList();
final locations = visibleEntries.map((entry) => entry.addressDetails).whereNotNull().toList();
final updatedPlaces = locations.map((address) => address.place).whereNotNull().where((v) => v.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase);
if (!listEquals(updatedPlaces, sortedPlaces)) {
sortedPlaces = List.unmodifiable(updatedPlaces);
@ -177,67 +177,6 @@ mixin LocationMixin on SourceBase {
eventBus.fire(CountriesChangedEvent());
}
}
// filter summary
// by country code
final Map<String, int> _filterEntryCountMap = {}, _filterSizeMap = {};
final Map<String, AvesEntry?> _filterRecentEntryMap = {};
void invalidateCountryFilterSummary({
Set<AvesEntry>? entries,
Set<String>? countryCodes,
bool notify = true,
}) {
if (_filterEntryCountMap.isEmpty && _filterSizeMap.isEmpty && _filterRecentEntryMap.isEmpty) return;
if (entries == null && countryCodes == null) {
_filterEntryCountMap.clear();
_filterSizeMap.clear();
_filterRecentEntryMap.clear();
} else {
countryCodes ??= {};
if (entries != null) {
countryCodes.addAll(entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.countryCode).whereNotNull());
}
countryCodes.forEach((countryCode) {
_filterEntryCountMap.remove(countryCode);
_filterSizeMap.remove(countryCode);
_filterRecentEntryMap.remove(countryCode);
});
}
if (notify) {
eventBus.fire(CountrySummaryInvalidatedEvent(countryCodes));
}
}
int countryEntryCount(LocationFilter filter) {
final countryCode = filter.countryCode;
if (countryCode == null) return 0;
return _filterEntryCountMap.putIfAbsent(countryCode, () => visibleEntries.where(filter.test).length);
}
int countrySize(LocationFilter filter) {
final countryCode = filter.countryCode;
if (countryCode == null) return 0;
return _filterSizeMap.putIfAbsent(countryCode, () => visibleEntries.where(filter.test).map((v) => v.sizeBytes).sum);
}
AvesEntry? countryRecentEntry(LocationFilter filter) {
final countryCode = filter.countryCode;
if (countryCode == null) return null;
return _filterRecentEntryMap.putIfAbsent(countryCode, () => sortedEntriesByDate.firstWhereOrNull(filter.test));
}
}
class AddressMetadataChangedEvent {}
class PlacesChangedEvent {}
class CountriesChangedEvent {}
class CountrySummaryInvalidatedEvent {
final Set<String>? countryCodes;
const CountrySummaryInvalidatedEvent(this.countryCodes);
}

View file

@ -0,0 +1,60 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/utils/collection_utils.dart';
import 'package:collection/collection.dart';
mixin PlaceMixin on SourceBase {
// filter summary
// by place
final Map<String, int> _filterEntryCountMap = {}, _filterSizeMap = {};
final Map<String, AvesEntry?> _filterRecentEntryMap = {};
void invalidatePlaceFilterSummary({
Set<AvesEntry>? entries,
Set<String>? places,
bool notify = true,
}) {
if (_filterEntryCountMap.isEmpty && _filterSizeMap.isEmpty && _filterRecentEntryMap.isEmpty) return;
if (entries == null && places == null) {
_filterEntryCountMap.clear();
_filterSizeMap.clear();
_filterRecentEntryMap.clear();
} else {
places ??= {};
if (entries != null) {
places.addAll(entries.map((entry) => entry.addressDetails?.place).whereNotNull());
}
places.forEach((place) {
_filterEntryCountMap.remove(place);
_filterSizeMap.remove(place);
_filterRecentEntryMap.remove(place);
});
}
if (notify) {
eventBus.fire(PlaceSummaryInvalidatedEvent(places));
}
}
int placeEntryCount(LocationFilter filter) {
return _filterEntryCountMap.putIfAbsent(filter.place, () => visibleEntries.where(filter.test).length);
}
int placeSize(LocationFilter filter) {
return _filterSizeMap.putIfAbsent(filter.place, () => visibleEntries.where(filter.test).map((v) => v.sizeBytes).sum);
}
AvesEntry? placeRecentEntry(LocationFilter filter) {
return _filterRecentEntryMap.putIfAbsent(filter.place, () => sortedEntriesByDate.firstWhereOrNull(filter.test));
}
}
class PlacesChangedEvent {}
class PlaceSummaryInvalidatedEvent {
final Set<String>? places;
const PlaceSummaryInvalidatedEvent(this.places);
}

View file

@ -36,6 +36,8 @@ class AIcons {
static const IconData language = Icons.translate_outlined;
static const IconData location = Icons.place_outlined;
static const IconData locationUnlocated = Icons.location_off_outlined;
static const IconData country = Icons.flag_outlined;
static const IconData place = Icons.place_outlined;
static const IconData mainStorage = Icons.smartphone_outlined;
static const IconData mimeType = Icons.code_outlined;
static const IconData opacity = Icons.opacity;

View file

@ -96,6 +96,7 @@ class AvesFilterChip extends StatefulWidget {
final actions = [
if (filter is AlbumFilter) ChipAction.goToAlbumPage,
if ((filter is LocationFilter && filter.level == LocationLevel.country)) ChipAction.goToCountryPage,
if ((filter is LocationFilter && filter.level == LocationLevel.place)) ChipAction.goToPlacePage,
if (filter is TagFilter) ChipAction.goToTagPage,
ChipAction.reverse,
ChipAction.hide,

View file

@ -3,6 +3,7 @@ import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/places_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves/widgets/viewer/info/common.dart';
import 'package:flutter/material.dart';
@ -51,6 +52,7 @@ class DebugSettingsSection extends StatelessWidget {
'tileExtent - Collection': '${settings.getTileExtent(CollectionPage.routeName)}',
'tileExtent - Albums': '${settings.getTileExtent(AlbumListPage.routeName)}',
'tileExtent - Countries': '${settings.getTileExtent(CountryListPage.routeName)}',
'tileExtent - Places': '${settings.getTileExtent(PlaceListPage.routeName)}',
'tileExtent - Tags': '${settings.getTileExtent(TagListPage.routeName)}',
'infoMapZoom': '${settings.infoMapZoom}',
'collectionSelectionQuickActions': '${settings.collectionSelectionQuickActions}',

View file

@ -11,6 +11,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/places_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -23,6 +24,7 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin {
switch (action) {
case ChipAction.goToAlbumPage:
case ChipAction.goToCountryPage:
case ChipAction.goToPlacePage:
case ChipAction.goToTagPage:
case ChipAction.reverse:
return true;
@ -42,6 +44,9 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin {
case ChipAction.goToCountryPage:
_goTo(context, filter, CountryListPage.routeName, (context) => const CountryListPage());
break;
case ChipAction.goToPlacePage:
_goTo(context, filter, PlaceListPage.routeName, (context) => const PlaceListPage());
break;
case ChipAction.goToTagPage:
_goTo(context, filter, TagListPage.routeName, (context) => const TagListPage());
break;

View file

@ -0,0 +1,33 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
import 'package:aves/widgets/filter_grids/places_page.dart';
class PlaceChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter> {
final Iterable<FilterGridItem<LocationFilter>> _items;
PlaceChipSetActionDelegate(Iterable<FilterGridItem<LocationFilter>> items) : _items = items;
@override
Iterable<FilterGridItem<LocationFilter>> get allItems => _items;
@override
ChipSortFactor get sortFactor => settings.placeSortFactor;
@override
set sortFactor(ChipSortFactor factor) => settings.placeSortFactor = factor;
@override
bool get sortReverse => settings.placeSortReverse;
@override
set sortReverse(bool value) => settings.placeSortReverse = value;
@override
TileLayout get tileLayout => settings.getTileLayout(PlaceListPage.routeName);
@override
set tileLayout(TileLayout tileLayout) => settings.setTileLayout(PlaceListPage.routeName, tileLayout);
}

View file

@ -7,7 +7,7 @@ import 'package:aves/model/filters/location.dart';
import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/source/album.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/location.dart';
import 'package:aves/model/source/location/country.dart';
import 'package:aves/model/source/tag.dart';
import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/theme/durations.dart';

View file

@ -3,7 +3,7 @@ import 'package:aves/model/filters/location.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/location.dart';
import 'package:aves/model/source/location/country.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/empty.dart';
@ -43,7 +43,7 @@ class CountryListPage extends StatelessWidget {
filterSections: _groupToSections(gridItems),
applyQuery: applyQuery,
emptyBuilder: () => EmptyContent(
icon: AIcons.location,
icon: AIcons.country,
text: context.l10n.countryEmpty,
),
);

View file

@ -0,0 +1,80 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/location/place.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/place_set.dart';
import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
class PlaceListPage extends StatelessWidget {
static const routeName = '/places';
const PlaceListPage({super.key});
@override
Widget build(BuildContext context) {
final source = context.read<CollectionSource>();
return Selector<Settings, Tuple3<ChipSortFactor, bool, Set<CollectionFilter>>>(
selector: (context, s) => Tuple3(s.placeSortFactor, s.placeSortReverse, s.pinnedFilters),
shouldRebuild: (t1, t2) {
// `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within `TupleN`
const eq = DeepCollectionEquality();
return !(eq.equals(t1.item1, t2.item1) && eq.equals(t1.item2, t2.item2) && eq.equals(t1.item3, t2.item3));
},
builder: (context, s, child) {
return StreamBuilder(
stream: source.eventBus.on<PlacesChangedEvent>(),
builder: (context, snapshot) {
final gridItems = _getGridItems(source);
return FilterNavigationPage<LocationFilter, PlaceChipSetActionDelegate>(
source: source,
title: context.l10n.placePageTitle,
sortFactor: settings.placeSortFactor,
actionDelegate: PlaceChipSetActionDelegate(gridItems),
filterSections: _groupToSections(gridItems),
applyQuery: applyQuery,
emptyBuilder: () => EmptyContent(
icon: AIcons.place,
text: context.l10n.placeEmpty,
),
);
},
);
},
);
}
List<FilterGridItem<LocationFilter>> applyQuery(BuildContext context, List<FilterGridItem<LocationFilter>> filters, String query) {
return filters.where((item) => item.filter.getLabel(context).toUpperCase().contains(query)).toList();
}
List<FilterGridItem<LocationFilter>> _getGridItems(CollectionSource source) {
final filters = source.sortedPlaces.map((location) => LocationFilter(LocationLevel.place, location)).toSet();
return FilterNavigationPage.sort(settings.placeSortFactor, settings.placeSortReverse, source, filters);
}
static Map<ChipSectionKey, List<FilterGridItem<LocationFilter>>> _groupToSections(Iterable<FilterGridItem<LocationFilter>> sortedMapEntries) {
final pinned = settings.pinnedFilters.whereType<LocationFilter>();
final byPin = groupBy<FilterGridItem<LocationFilter>, bool>(sortedMapEntries, (e) => pinned.contains(e.filter));
final pinnedMapEntries = (byPin[true] ?? []);
final unpinnedMapEntries = (byPin[false] ?? []);
return {
if (pinnedMapEntries.isNotEmpty || unpinnedMapEntries.isNotEmpty)
const ChipSectionKey(): [
...pinnedMapEntries,
...unpinnedMapEntries,
],
};
}
}

View file

@ -6,7 +6,8 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/album.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/location.dart';
import 'package:aves/model/source/location/country.dart';
import 'package:aves/model/source/location/place.dart';
import 'package:aves/model/source/tag.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
@ -19,6 +20,7 @@ import 'package:aves/widgets/common/identity/aves_logo.dart';
import 'package:aves/widgets/debug/app_debug_page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/places_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves/widgets/navigation/drawer/collection_nav_tile.dart';
import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart';
@ -242,6 +244,12 @@ class _AppDrawerState extends State<AppDrawer> {
builder: (context, _) => Text('${source.sortedCountries.length}'),
);
break;
case PlaceListPage.routeName:
trailing = StreamBuilder(
stream: source.eventBus.on<PlacesChangedEvent>(),
builder: (context, _) => Text('${source.sortedPlaces.length}'),
);
break;
case TagListPage.routeName:
trailing = StreamBuilder(
stream: source.eventBus.on<TagsChangedEvent>(),

View file

@ -7,6 +7,7 @@ import 'package:aves/widgets/common/search/route.dart';
import 'package:aves/widgets/debug/app_debug_page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/places_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves/widgets/navigation/drawer/tile.dart';
import 'package:aves/widgets/search/search_delegate.dart';
@ -88,6 +89,8 @@ class PageNavTile extends StatelessWidget {
return (_) => const AlbumListPage();
case CountryListPage.routeName:
return (_) => const CountryListPage();
case PlaceListPage.routeName:
return (_) => const PlaceListPage();
case TagListPage.routeName:
return (_) => const TagListPage();
case SettingsPage.routeName:

View file

@ -9,6 +9,7 @@ import 'package:aves/widgets/common/search/page.dart';
import 'package:aves/widgets/debug/app_debug_page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/places_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves/widgets/settings/settings_page.dart';
import 'package:flutter/material.dart';
@ -35,6 +36,8 @@ class NavigationDisplay {
return l10n.drawerAlbumPage;
case CountryListPage.routeName:
return l10n.drawerCountryPage;
case PlaceListPage.routeName:
return l10n.drawerPlacePage;
case TagListPage.routeName:
return l10n.drawerTagPage;
case SettingsPage.routeName:
@ -55,7 +58,9 @@ class NavigationDisplay {
case AlbumListPage.routeName:
return AIcons.album;
case CountryListPage.routeName:
return AIcons.location;
return AIcons.country;
case PlaceListPage.routeName:
return AIcons.place;
case TagListPage.routeName:
return AIcons.tag;
case SettingsPage.routeName:

View file

@ -15,7 +15,8 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/album.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/location.dart';
import 'package:aves/model/source/location/country.dart';
import 'package:aves/model/source/location/place.dart';
import 'package:aves/model/source/tag.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/widgets/collection/collection_page.dart';

View file

@ -6,6 +6,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/search/page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/places_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves/widgets/navigation/drawer/app_drawer.dart';
import 'package:aves/widgets/navigation/drawer/tile.dart';
@ -40,6 +41,7 @@ class _NavigationDrawerEditorPageState extends State<NavigationDrawerEditorPage>
static const Set<String> _pageOptions = {
AlbumListPage.routeName,
CountryListPage.routeName,
PlaceListPage.routeName,
TagListPage.routeName,
SearchPage.routeName,
};

View file

@ -14,6 +14,7 @@
"chipActionDelete",
"chipActionGoToAlbumPage",
"chipActionGoToCountryPage",
"chipActionGoToPlacePage",
"chipActionGoToTagPage",
"chipActionFilterOut",
"chipActionFilterIn",
@ -334,6 +335,7 @@
"drawerCollectionSphericalVideos",
"drawerAlbumPage",
"drawerCountryPage",
"drawerPlacePage",
"drawerTagPage",
"sortByDate",
"sortByName",
@ -369,6 +371,8 @@
"newFilterBanner",
"countryPageTitle",
"countryEmpty",
"placePageTitle",
"placeEmpty",
"tagPageTitle",
"tagEmpty",
"binPageTitle",
@ -591,6 +595,7 @@
],
"cs": [
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -609,11 +614,15 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty",
"settingsConfirmationVaultDataLoss",
"settingsDisablingBinWarningDialogMessage"
],
"de": [
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -632,11 +641,29 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty",
"settingsConfirmationVaultDataLoss",
"settingsDisablingBinWarningDialogMessage"
],
"el": [
"chipActionGoToPlacePage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty"
],
"es": [
"chipActionGoToPlacePage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty"
],
"eu": [
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -655,12 +682,16 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty",
"settingsConfirmationVaultDataLoss",
"settingsDisablingBinWarningDialogMessage"
],
"fa": [
"clearTooltip",
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -858,6 +889,7 @@
"drawerCollectionSphericalVideos",
"drawerAlbumPage",
"drawerCountryPage",
"drawerPlacePage",
"drawerTagPage",
"sortByDate",
"sortByName",
@ -892,6 +924,8 @@
"newFilterBanner",
"countryPageTitle",
"countryEmpty",
"placePageTitle",
"placeEmpty",
"tagPageTitle",
"tagEmpty",
"binPageTitle",
@ -1120,8 +1154,16 @@
"filePickerUseThisFolder"
],
"fr": [
"chipActionGoToPlacePage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty"
],
"gl": [
"columnCount",
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -1344,6 +1386,7 @@
"drawerCollectionSphericalVideos",
"drawerAlbumPage",
"drawerCountryPage",
"drawerPlacePage",
"drawerTagPage",
"sortByDate",
"sortByName",
@ -1379,6 +1422,8 @@
"newFilterBanner",
"countryPageTitle",
"countryEmpty",
"placePageTitle",
"placeEmpty",
"tagPageTitle",
"tagEmpty",
"binPageTitle",
@ -1646,6 +1691,7 @@
"chipActionDelete",
"chipActionGoToAlbumPage",
"chipActionGoToCountryPage",
"chipActionGoToPlacePage",
"chipActionGoToTagPage",
"chipActionFilterOut",
"chipActionFilterIn",
@ -1966,6 +2012,7 @@
"drawerCollectionSphericalVideos",
"drawerAlbumPage",
"drawerCountryPage",
"drawerPlacePage",
"drawerTagPage",
"sortByDate",
"sortByName",
@ -2001,6 +2048,8 @@
"newFilterBanner",
"countryPageTitle",
"countryEmpty",
"placePageTitle",
"placeEmpty",
"tagPageTitle",
"tagEmpty",
"binPageTitle",
@ -2235,7 +2284,15 @@
"filePickerUseThisFolder"
],
"id": [
"chipActionGoToPlacePage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty"
],
"it": [
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -2254,12 +2311,16 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty",
"settingsConfirmationVaultDataLoss",
"settingsDisablingBinWarningDialogMessage"
],
"ja": [
"columnCount",
"chipActionGoToPlacePage",
"chipActionFilterIn",
"chipActionLock",
"chipActionCreateVault",
@ -2288,6 +2349,9 @@
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"tooManyItemsErrorDialogMessage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty",
"settingsModificationWarningDialogMessage",
"settingsConfirmationVaultDataLoss",
"settingsViewerShowDescription",
@ -2298,8 +2362,16 @@
"settingsWidgetDisplayedItem"
],
"ko": [
"chipActionGoToPlacePage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty"
],
"lt": [
"columnCount",
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -2322,6 +2394,9 @@
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"tooManyItemsErrorDialogMessage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty",
"settingsModificationWarningDialogMessage",
"settingsConfirmationVaultDataLoss",
"settingsViewerShowDescription",
@ -2332,6 +2407,7 @@
],
"nb": [
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -2350,12 +2426,16 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty",
"settingsConfirmationVaultDataLoss",
"settingsDisablingBinWarningDialogMessage"
],
"nl": [
"columnCount",
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -2389,6 +2469,9 @@
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"tooManyItemsErrorDialogMessage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty",
"settingsModificationWarningDialogMessage",
"settingsConfirmationVaultDataLoss",
"settingsViewerShowRatingTags",
@ -2405,6 +2488,7 @@
"nn": [
"columnCount",
"sourceStateCataloguing",
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -2504,6 +2588,7 @@
"drawerCollectionSphericalVideos",
"drawerAlbumPage",
"drawerCountryPage",
"drawerPlacePage",
"drawerTagPage",
"sortByDate",
"sortByName",
@ -2539,6 +2624,8 @@
"newFilterBanner",
"countryPageTitle",
"countryEmpty",
"placePageTitle",
"placeEmpty",
"tagPageTitle",
"tagEmpty",
"binPageTitle",
@ -2709,8 +2796,16 @@
"wallpaperUseScrollEffect"
],
"pl": [
"chipActionGoToPlacePage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty"
],
"pt": [
"columnCount",
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -2730,12 +2825,23 @@
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"tooManyItemsErrorDialogMessage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty",
"settingsConfirmationVaultDataLoss",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsDisablingBinWarningDialogMessage"
],
"ro": [
"chipActionGoToPlacePage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty"
],
"ru": [
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -2757,6 +2863,9 @@
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"tooManyItemsErrorDialogMessage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty",
"settingsModificationWarningDialogMessage",
"settingsConfirmationVaultDataLoss",
"settingsVideoGestureVerticalDragBrightnessVolume",
@ -2768,6 +2877,7 @@
"itemCount",
"columnCount",
"timeSeconds",
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -2905,6 +3015,7 @@
"drawerCollectionSphericalVideos",
"drawerAlbumPage",
"drawerCountryPage",
"drawerPlacePage",
"drawerTagPage",
"sortByDate",
"sortByName",
@ -2940,6 +3051,8 @@
"newFilterBanner",
"countryPageTitle",
"countryEmpty",
"placePageTitle",
"placeEmpty",
"tagPageTitle",
"tagEmpty",
"binPageTitle",
@ -3182,6 +3295,7 @@
"timeDays",
"focalLength",
"applyButtonLabel",
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -3250,6 +3364,7 @@
"drawerCollectionSphericalVideos",
"drawerAlbumPage",
"drawerCountryPage",
"drawerPlacePage",
"drawerTagPage",
"sortByDate",
"sortByName",
@ -3285,6 +3400,8 @@
"newFilterBanner",
"countryPageTitle",
"countryEmpty",
"placePageTitle",
"placeEmpty",
"tagPageTitle",
"tagEmpty",
"binPageTitle",
@ -3520,6 +3637,7 @@
],
"tr": [
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -3538,11 +3656,22 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty",
"settingsConfirmationVaultDataLoss",
"settingsDisablingBinWarningDialogMessage"
],
"uk": [
"chipActionGoToPlacePage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty"
],
"zh": [
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -3564,6 +3693,9 @@
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"tooManyItemsErrorDialogMessage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty",
"settingsModificationWarningDialogMessage",
"settingsConfirmationVaultDataLoss",
"settingsViewerShowDescription",
@ -3575,6 +3707,7 @@
"zh_Hant": [
"columnCount",
"chipActionGoToPlacePage",
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
@ -3596,6 +3729,9 @@
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"tooManyItemsErrorDialogMessage",
"drawerPlacePage",
"placePageTitle",
"placeEmpty",
"settingsModificationWarningDialogMessage",
"settingsConfirmationVaultDataLoss",
"settingsViewerShowDescription",