diff --git a/i18n/en.json b/i18n/en.json index ad48a969913f0..7dca9170f6aae 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -766,8 +766,10 @@ "go_to_search": "Go to search", "go_to_folder": "Go to folder", "group_albums_by": "Group albums by...", + "group_country": "Group by country", "group_no": "No grouping", "group_owner": "Group by owner", + "group_places_by": "Group places by...", "group_year": "Group by year", "has_quota": "Has quota", "hi_user": "Hi {name} ({email})", @@ -985,6 +987,7 @@ "pick_a_location": "Pick a location", "place": "Place", "places": "Places", + "places_count": "{count, plural, one {{count, number} Place} other {{count, number} Places}}", "play": "Play", "play_memories": "Play memories", "play_motion_photo": "Play Motion Photo", @@ -1276,6 +1279,7 @@ "unfavorite": "Unfavorite", "unhide_person": "Unhide person", "unknown": "Unknown", + "unknown_country": "Unknown Country", "unknown_year": "Unknown Year", "unlimited": "Unlimited", "unlink_motion_video": "Unlink motion video", diff --git a/web/src/lib/components/places-page/places-card-group.svelte b/web/src/lib/components/places-page/places-card-group.svelte new file mode 100644 index 0000000000000..87b9870888f75 --- /dev/null +++ b/web/src/lib/components/places-page/places-card-group.svelte @@ -0,0 +1,67 @@ + + +{#if group} +
+ +
+
+{/if} + +
+ {#if !isCollapsed} +
+ {#each places as item} + {@const city = item.exifInfo?.city} + +
+ {city} +
+ + {city} + +
+ {/each} +
+ {/if} +
diff --git a/web/src/lib/components/places-page/places-controls.svelte b/web/src/lib/components/places-page/places-controls.svelte new file mode 100644 index 0000000000000..7feaafb7f79f5 --- /dev/null +++ b/web/src/lib/components/places-page/places-controls.svelte @@ -0,0 +1,92 @@ + + + + + + + ({ + title: placesGroupByNames[id], + icon: groupIcon, + disabled: isDisabled(), + })} +/> + +{#if getSelectedPlacesGroupOption($placesViewSettings) !== PlacesGroupBy.None} + + + + +{/if} diff --git a/web/src/lib/components/places-page/places-list.svelte b/web/src/lib/components/places-page/places-list.svelte new file mode 100644 index 0000000000000..27eea3c5a8e8e --- /dev/null +++ b/web/src/lib/components/places-page/places-list.svelte @@ -0,0 +1,121 @@ + + +{#if hasPlaces} + + {#if placesGroupOption === PlacesGroupBy.None} + + {:else} + {#each groupedPlaces as placeGroup (placeGroup.id)} + + {/each} + {/if} +{:else} +
+
+ +

{$t('no_places')}

+
+
+{/if} diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index 1ec0853dc0696..818800755cc79 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -101,6 +101,14 @@ export interface AlbumViewSettings { }; } +export interface PlacesViewSettings { + groupBy: string; + collapsedGroups: { + // Grouping Option => Array + [group: string]: string[]; + }; +} + export interface SidebarSettings { people: boolean; sharing: boolean; @@ -147,6 +155,16 @@ export const albumViewSettings = persisted('album-view-settin collapsedGroups: {}, }); +export enum PlacesGroupBy { + None = 'None', + Country = 'Country', +} + +export const placesViewSettings = persisted('places-view-settings', { + groupBy: PlacesGroupBy.None, + collapsedGroups: {}, +}); + export const showDeleteModal = persisted('delete-confirm-dialog', true, {}); export const alwaysLoadOriginalFile = persisted('always-load-original-file', false, {}); diff --git a/web/src/lib/utils/places-utils.ts b/web/src/lib/utils/places-utils.ts new file mode 100644 index 0000000000000..625f42d147a89 --- /dev/null +++ b/web/src/lib/utils/places-utils.ts @@ -0,0 +1,95 @@ +import { PlacesGroupBy, placesViewSettings, type PlacesViewSettings } from '$lib/stores/preferences.store'; +import { type AssetResponseDto } from '@immich/sdk'; +import { get } from 'svelte/store'; + +/** + * -------------- + * Places Grouping + * -------------- + */ +export interface PlacesGroup { + id: string; + name: string; + places: AssetResponseDto[]; +} + +export interface PlacesGroupOptionMetadata { + id: PlacesGroupBy; + isDisabled: () => boolean; +} + +export const groupOptionsMetadata: PlacesGroupOptionMetadata[] = [ + { + id: PlacesGroupBy.None, + isDisabled: () => false, + }, + { + id: PlacesGroupBy.Country, + isDisabled: () => false, + }, +]; + +export const findGroupOptionMetadata = (groupBy: string) => { + // Default is no grouping + const defaultGroupOption = groupOptionsMetadata[0]; + return groupOptionsMetadata.find(({ id }) => groupBy === id) ?? defaultGroupOption; +}; + +export const getSelectedPlacesGroupOption = (settings: PlacesViewSettings) => { + const defaultGroupOption = PlacesGroupBy.None; + const albumGroupOption = settings.groupBy ?? defaultGroupOption; + + if (findGroupOptionMetadata(albumGroupOption).isDisabled()) { + return defaultGroupOption; + } + return albumGroupOption; +}; + +/** + * ---------------------------- + * Places Groups Collapse/Expand + * ---------------------------- + */ +const getCollapsedPlacesGroups = (settings: PlacesViewSettings) => { + settings.collapsedGroups ??= {}; + const { collapsedGroups, groupBy } = settings; + collapsedGroups[groupBy] ??= []; + return collapsedGroups[groupBy]; +}; + +export const isPlacesGroupCollapsed = (settings: PlacesViewSettings, groupId: string) => { + if (settings.groupBy === PlacesGroupBy.None) { + return false; + } + return getCollapsedPlacesGroups(settings).includes(groupId); +}; + +export const togglePlacesGroupCollapsing = (groupId: string) => { + const settings = get(placesViewSettings); + if (settings.groupBy === PlacesGroupBy.None) { + return; + } + const collapsedGroups = getCollapsedPlacesGroups(settings); + const groupIndex = collapsedGroups.indexOf(groupId); + if (groupIndex === -1) { + // Collapse + collapsedGroups.push(groupId); + } else { + // Expand + collapsedGroups.splice(groupIndex, 1); + } + placesViewSettings.set(settings); +}; + +export const collapseAllPlacesGroups = (groupIds: string[]) => { + placesViewSettings.update((settings) => { + const collapsedGroups = getCollapsedPlacesGroups(settings); + collapsedGroups.length = 0; + collapsedGroups.push(...groupIds); + return settings; + }); +}; + +export const expandAllPlacesGroups = () => { + collapseAllPlacesGroups([]); +}; diff --git a/web/src/routes/(user)/places/+page.svelte b/web/src/routes/(user)/places/+page.svelte index 1808755482672..636f28013fedc 100644 --- a/web/src/routes/(user)/places/+page.svelte +++ b/web/src/routes/(user)/places/+page.svelte @@ -1,13 +1,12 @@ - - {#if hasPlaces} -
- {#each places as item (item.id)} - {@const city = item.exifInfo.city} - -
- {city} -
- - {city} - -
- {/each} -
- {:else} -
-
- -

{$t('no_places')}

-
+ + {#snippet buttons()} +
+
- {/if} + {/snippet} + +