Compare commits

...

1 Commits

Author SHA1 Message Date
Mees Frensel
1bdbf8656b chore(web): migrate albums page to ui's Select 2026-06-15 17:41:51 +02:00
2 changed files with 77 additions and 73 deletions

View File

@@ -2314,6 +2314,7 @@
"support_third_party_description": "Your Immich installation was packaged by a third-party. Issues you experience may be caused by that package, so please raise issues with them in the first instance using the links below.", "support_third_party_description": "Your Immich installation was packaged by a third-party. Issues you experience may be caused by that package, so please raise issues with them in the first instance using the links below.",
"supporter": "Supporter", "supporter": "Supporter",
"swap_merge_direction": "Swap merge direction", "swap_merge_direction": "Swap merge direction",
"switch_sort_order": "Switch sort order from {from, select, asc {ascending} desc {descending} other {unknown}} to {to, select, asc {ascending} desc {descending} other {unknown}}",
"sync": "Sync", "sync": "Sync",
"sync_albums": "Sync albums", "sync_albums": "Sync albums",
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import Dropdown from '$lib/elements/Dropdown.svelte';
import GroupTab from '$lib/elements/GroupTab.svelte'; import GroupTab from '$lib/elements/GroupTab.svelte';
import SearchBar from '$lib/elements/SearchBar.svelte'; import SearchBar from '$lib/elements/SearchBar.svelte';
import { import {
@@ -11,8 +10,6 @@
SortOrder, SortOrder,
} from '$lib/stores/preferences.store'; } from '$lib/stores/preferences.store';
import { import {
type AlbumGroupOptionMetadata,
type AlbumSortOptionMetadata,
collapseAllAlbumGroups, collapseAllAlbumGroups,
createAlbumAndRedirect, createAlbumAndRedirect,
expandAllAlbumGroups, expandAllAlbumGroups,
@@ -23,7 +20,7 @@
groupOptionsMetadata, groupOptionsMetadata,
sortOptionsMetadata, sortOptionsMetadata,
} from '$lib/utils/album-utils'; } from '$lib/utils/album-utils';
import { Button, IconButton, Text } from '@immich/ui'; import { Button, IconButton, Select, Text } from '@immich/ui';
import { import {
mdiArrowDownThin, mdiArrowDownThin,
mdiArrowUpThin, mdiArrowUpThin,
@@ -37,12 +34,12 @@
mdiViewGridOutline, mdiViewGridOutline,
} from '@mdi/js'; } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition'; import { slide } from 'svelte/transition';
interface Props { type Props = {
albumGroups: string[]; albumGroups: string[];
searchQuery: string; searchQuery: string;
} };
let { albumGroups, searchQuery = $bindable() }: Props = $props(); let { albumGroups, searchQuery = $bindable() }: Props = $props();
@@ -55,21 +52,12 @@
Object.keys(albumFilterNames).find((key) => albumFilterNames[key as AlbumFilter] === filter) ?? defaultFilter; Object.keys(albumFilterNames).find((key) => albumFilterNames[key as AlbumFilter] === filter) ?? defaultFilter;
}; };
const handleChangeGroupBy = ({ id, defaultOrder }: AlbumGroupOptionMetadata) => { const handleChangeGroupBy = (id: string) =>
if ($albumViewSettings.groupBy === id) { ($albumViewSettings.groupOrder = findGroupOptionMetadata(id).defaultOrder);
$albumViewSettings.groupOrder = flipOrdering($albumViewSettings.groupOrder); const handleChangeSortBy = (id: string) => {
} else { $albumViewSettings.sortOrder = findSortOptionMetadata(id).defaultOrder;
$albumViewSettings.groupBy = id; if (findGroupOptionMetadata($albumViewSettings.groupBy).isDisabled()) {
$albumViewSettings.groupOrder = defaultOrder; $albumViewSettings.groupBy = AlbumGroupBy.None;
}
};
const handleChangeSortBy = ({ id, defaultOrder }: AlbumSortOptionMetadata) => {
if ($albumViewSettings.sortBy === id) {
$albumViewSettings.sortOrder = flipOrdering($albumViewSettings.sortOrder);
} else {
$albumViewSettings.sortBy = id;
$albumViewSettings.sortOrder = defaultOrder;
} }
}; };
@@ -78,25 +66,32 @@
$albumViewSettings.view === AlbumViewMode.Cover ? AlbumViewMode.List : AlbumViewMode.Cover; $albumViewSettings.view === AlbumViewMode.Cover ? AlbumViewMode.List : AlbumViewMode.Cover;
}; };
let groupIcon = $derived.by(() => { const groupByOptions = $derived.by(() => {
if (selectedGroupOption?.id === AlbumGroupBy.None) { // eslint-disable-next-line @typescript-eslint/no-unused-expressions
$albumViewSettings.sortBy; // ensure disabled status gets updated on change of sortBy
return groupOptionsMetadata.map(({ id, isDisabled }) => ({
value: id,
label: albumGroupByNames[id],
disabled: isDisabled(),
}));
});
const groupIcon = $derived.by(() => {
if ($albumViewSettings.groupBy === AlbumGroupBy.None) {
return mdiFolderRemoveOutline; return mdiFolderRemoveOutline;
} }
return $albumViewSettings.groupOrder === SortOrder.Desc ? mdiFolderArrowDownOutline : mdiFolderArrowUpOutline; return $albumViewSettings.groupOrder === SortOrder.Desc ? mdiFolderArrowDownOutline : mdiFolderArrowUpOutline;
}); });
let albumFilterNames: Record<AlbumFilter, string> = $derived({ const albumFilterNames: Record<AlbumFilter, string> = $derived({
[AlbumFilter.All]: $t('all'), [AlbumFilter.All]: $t('all'),
[AlbumFilter.Owned]: $t('owned'), [AlbumFilter.Owned]: $t('owned'),
[AlbumFilter.Shared]: $t('shared'), [AlbumFilter.Shared]: $t('shared'),
}); });
let selectedFilterOption = $derived(albumFilterNames[findFilterOption($albumViewSettings.filter)]); let selectedFilterOption = $derived(albumFilterNames[findFilterOption($albumViewSettings.filter)]);
let selectedSortOption = $derived(findSortOptionMetadata($albumViewSettings.sortBy));
let selectedGroupOption = $derived(findGroupOptionMetadata($albumViewSettings.groupBy));
let sortIcon = $derived($albumViewSettings.sortOrder === SortOrder.Desc ? mdiArrowDownThin : mdiArrowUpThin);
let albumSortByNames: Record<AlbumSortBy, string> = $derived({ const albumSortByNames: Record<AlbumSortBy, string> = $derived({
[AlbumSortBy.Title]: $t('sort_title'), [AlbumSortBy.Title]: $t('sort_title'),
[AlbumSortBy.ItemCount]: $t('sort_items'), [AlbumSortBy.ItemCount]: $t('sort_items'),
[AlbumSortBy.DateModified]: $t('sort_modified'), [AlbumSortBy.DateModified]: $t('sort_modified'),
@@ -105,7 +100,7 @@
[AlbumSortBy.OldestPhoto]: $t('sort_oldest'), [AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
}); });
let albumGroupByNames: Record<AlbumGroupBy, string> = $derived({ const albumGroupByNames: Record<AlbumGroupBy, string> = $derived({
[AlbumGroupBy.None]: $t('group_no'), [AlbumGroupBy.None]: $t('group_no'),
[AlbumGroupBy.Owner]: $t('group_owner'), [AlbumGroupBy.Owner]: $t('group_owner'),
[AlbumGroupBy.Year]: $t('group_year'), [AlbumGroupBy.Year]: $t('group_year'),
@@ -139,37 +134,58 @@
</Button> </Button>
<!-- Sort Albums --> <!-- Sort Albums -->
<Dropdown <IconButton
title={$t('sort_albums_by')} icon={$albumViewSettings.sortOrder === SortOrder.Desc ? mdiArrowDownThin : mdiArrowUpThin}
options={Object.values(sortOptionsMetadata)} aria-label={$t('switch_sort_order', {
selectedOption={selectedSortOption} values: {
onSelect={handleChangeSortBy} from: $albumViewSettings.sortOrder,
render={({ id }) => ({ to: $albumViewSettings.sortOrder === SortOrder.Desc ? SortOrder.Asc : SortOrder.Desc,
title: albumSortByNames[id], },
icon: sortIcon,
})} })}
onclick={() => ($albumViewSettings.sortOrder = flipOrdering($albumViewSettings.sortOrder))}
variant="ghost"
color="secondary"
/> />
<div title={$t('sort_albums_by')}>
<Select
bind:value={$albumViewSettings.sortBy}
options={sortOptionsMetadata.map(({ id }) => ({ value: id, label: albumSortByNames[id] }))}
onChange={handleChangeSortBy}
class="w-fit min-w-45"
/>
</div>
<!-- Group Albums --> <!-- Group Albums -->
<Dropdown <IconButton
title={$t('group_albums_by')} icon={groupIcon}
options={Object.values(groupOptionsMetadata)} aria-label={$albumViewSettings.groupBy === AlbumGroupBy.None
selectedOption={selectedGroupOption} ? $t('not_available')
onSelect={handleChangeGroupBy} : $t('switch_sort_order', {
render={({ id, isDisabled }) => ({ values: {
title: albumGroupByNames[id], from: $albumViewSettings.groupOrder,
icon: groupIcon, to: $albumViewSettings.groupOrder === SortOrder.Desc ? SortOrder.Asc : SortOrder.Desc,
disabled: isDisabled(), },
})} })}
onclick={() => ($albumViewSettings.groupOrder = flipOrdering($albumViewSettings.groupOrder))}
disabled={$albumViewSettings.groupBy === AlbumGroupBy.None}
variant="ghost"
color="secondary"
/> />
<div title={$t('group_albums_by')}>
<Select
bind:value={$albumViewSettings.groupBy}
options={groupByOptions}
onChange={handleChangeGroupBy}
class="w-fit min-w-45"
/>
</div>
{#if getSelectedAlbumGroupOption($albumViewSettings) !== AlbumGroupBy.None} {#if getSelectedAlbumGroupOption($albumViewSettings) !== AlbumGroupBy.None}
<span in:fly={{ x: -50, duration: 250 }}> <span transition:slide={{ axis: 'x', duration: 250 }}>
<!-- Expand Album Groups -->
<div class="hidden gap-0 xl:flex"> <div class="hidden gap-0 xl:flex">
<!-- Expand Album Groups -->
<div class="block"> <div class="block">
<IconButton <IconButton
title={$t('expand_all')}
onclick={() => expandAllAlbumGroups()} onclick={() => expandAllAlbumGroups()}
variant="ghost" variant="ghost"
color="secondary" color="secondary"
@@ -182,7 +198,6 @@
<!-- Collapse Album Groups --> <!-- Collapse Album Groups -->
<div class="block"> <div class="block">
<IconButton <IconButton
title={$t('collapse_all')}
onclick={() => collapseAllAlbumGroups(albumGroups)} onclick={() => collapseAllAlbumGroups(albumGroups)}
variant="ghost" variant="ghost"
color="secondary" color="secondary"
@@ -196,24 +211,12 @@
{/if} {/if}
<!-- Cover/List Display Toggle --> <!-- Cover/List Display Toggle -->
{#if $albumViewSettings.view === AlbumViewMode.List} <Button
<Button leadingIcon={$albumViewSettings.view === AlbumViewMode.List ? mdiViewGridOutline : mdiFormatListBulletedSquare}
leadingIcon={mdiViewGridOutline} onclick={() => handleChangeListMode()}
onclick={() => handleChangeListMode()} size="small"
size="small" variant="ghost"
variant="ghost" color="secondary"
color="secondary" >
> <Text class="hidden md:block">{$albumViewSettings.view === AlbumViewMode.List ? $t('covers') : $t('list')}</Text>
<Text class="hidden md:block">{$t('covers')}</Text> </Button>
</Button>
{:else}
<Button
leadingIcon={mdiFormatListBulletedSquare}
onclick={() => handleChangeListMode()}
size="small"
variant="ghost"
color="secondary"
>
<Text class="hidden md:block">{$t('list')}</Text>
</Button>
{/if}