mirror of
https://github.com/immich-app/immich.git
synced 2025-12-23 01:11:36 +03:00
Merge branch 'main' of github.com:immich-app/immich into workflow-ui
This commit is contained in:
@@ -33,7 +33,6 @@
|
||||
import { groupBy } from 'lodash-es';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
interface Props {
|
||||
ownedAlbums?: AlbumResponseDto[];
|
||||
@@ -128,65 +127,45 @@
|
||||
},
|
||||
};
|
||||
|
||||
let albums: AlbumResponseDto[] = $state([]);
|
||||
let filteredAlbums: AlbumResponseDto[] = $state([]);
|
||||
let groupedAlbums: AlbumGroup[] = $state([]);
|
||||
let albums = $derived.by(() => {
|
||||
switch (userSettings.filter) {
|
||||
case AlbumFilter.Owned: {
|
||||
return ownedAlbums;
|
||||
}
|
||||
case AlbumFilter.Shared: {
|
||||
return sharedAlbums;
|
||||
}
|
||||
default: {
|
||||
const nonOwnedAlbums = sharedAlbums.filter((album) => album.ownerId !== $user.id);
|
||||
return nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums;
|
||||
}
|
||||
}
|
||||
});
|
||||
const normalizedSearchQuery = $derived(normalizeSearchString(searchQuery));
|
||||
let filteredAlbums = $derived(
|
||||
normalizedSearchQuery
|
||||
? albums.filter(({ albumName }) => normalizeSearchString(albumName).includes(normalizedSearchQuery))
|
||||
: albums,
|
||||
);
|
||||
|
||||
let albumGroupOption: string = $state(AlbumGroupBy.None);
|
||||
let albumGroupOption = $derived(getSelectedAlbumGroupOption(userSettings));
|
||||
let groupedAlbums = $derived.by(() => {
|
||||
const groupFunc = groupOptions[albumGroupOption] ?? groupOptions[AlbumGroupBy.None];
|
||||
const groupedAlbums = groupFunc(stringToSortOrder(userSettings.groupOrder), filteredAlbums);
|
||||
|
||||
let albumToShare: AlbumResponseDto | null = $state(null);
|
||||
return groupedAlbums.map((group) => ({
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
albums: sortAlbums(group.albums, { sortBy: userSettings.sortBy, orderBy: userSettings.sortOrder }),
|
||||
}));
|
||||
});
|
||||
|
||||
let contextMenuPosition: ContextMenuPosition = $state({ x: 0, y: 0 });
|
||||
let selectedAlbum: AlbumResponseDto | undefined = $state();
|
||||
let isOpen = $state(false);
|
||||
|
||||
// Step 1: Filter between Owned and Shared albums, or both.
|
||||
run(() => {
|
||||
switch (userSettings.filter) {
|
||||
case AlbumFilter.Owned: {
|
||||
albums = ownedAlbums;
|
||||
break;
|
||||
}
|
||||
case AlbumFilter.Shared: {
|
||||
albums = sharedAlbums;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const userId = $user.id;
|
||||
const nonOwnedAlbums = sharedAlbums.filter((album) => album.ownerId !== userId);
|
||||
albums = nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Step 2: Filter using the given search query.
|
||||
run(() => {
|
||||
if (searchQuery) {
|
||||
const searchAlbumNormalized = normalizeSearchString(searchQuery);
|
||||
|
||||
filteredAlbums = albums.filter((album) => {
|
||||
return normalizeSearchString(album.albumName).includes(searchAlbumNormalized);
|
||||
});
|
||||
} else {
|
||||
filteredAlbums = albums;
|
||||
}
|
||||
});
|
||||
|
||||
// Step 3: Group albums.
|
||||
run(() => {
|
||||
albumGroupOption = getSelectedAlbumGroupOption(userSettings);
|
||||
const groupFunc = groupOptions[albumGroupOption] ?? groupOptions[AlbumGroupBy.None];
|
||||
groupedAlbums = groupFunc(stringToSortOrder(userSettings.groupOrder), filteredAlbums);
|
||||
});
|
||||
|
||||
// Step 4: Sort albums amongst each group.
|
||||
run(() => {
|
||||
groupedAlbums = groupedAlbums.map((group) => ({
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
albums: sortAlbums(group.albums, { sortBy: userSettings.sortBy, orderBy: userSettings.sortOrder }),
|
||||
}));
|
||||
|
||||
// TODO get rid of this
|
||||
$effect(() => {
|
||||
albumGroupIds = groupedAlbums.map(({ id }) => id);
|
||||
});
|
||||
|
||||
@@ -231,7 +210,7 @@
|
||||
const result = await modalManager.show(AlbumShareModal, { album: selectedAlbum });
|
||||
switch (result?.action) {
|
||||
case 'sharedUsers': {
|
||||
await handleAddUsers(result.data);
|
||||
await handleAddUsers(selectedAlbum, result.data);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -300,22 +279,17 @@
|
||||
updateRecentAlbumInfo(album);
|
||||
};
|
||||
|
||||
const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => {
|
||||
if (!albumToShare) {
|
||||
return;
|
||||
}
|
||||
const handleAddUsers = async (album: AlbumResponseDto, albumUsers: AlbumUserAddDto[]) => {
|
||||
try {
|
||||
const album = await addUsersToAlbum({
|
||||
id: albumToShare.id,
|
||||
const updatedAlbum = await addUsersToAlbum({
|
||||
id: album.id,
|
||||
addUsersDto: {
|
||||
albumUsers,
|
||||
},
|
||||
});
|
||||
updateAlbumInfo(album);
|
||||
updateAlbumInfo(updatedAlbum);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_add_album_users'));
|
||||
} finally {
|
||||
albumToShare = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { setDifference, type TimelineDate } from '$lib/utils/timeline-util';
|
||||
import { AssetOrder } from '@immich/sdk';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import type { DayGroup } from './day-group.svelte';
|
||||
import type { MonthGroup } from './month-group.svelte';
|
||||
import type { TimelineAsset } from './types';
|
||||
@@ -10,8 +9,10 @@ export class GroupInsertionCache {
|
||||
[year: number]: { [month: number]: { [day: number]: DayGroup } };
|
||||
} = {};
|
||||
unprocessedAssets: TimelineAsset[] = [];
|
||||
changedDayGroups = new SvelteSet<DayGroup>();
|
||||
newDayGroups = new SvelteSet<DayGroup>();
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
changedDayGroups = new Set<DayGroup>();
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
newDayGroups = new Set<DayGroup>();
|
||||
|
||||
getDayGroup({ year, month, day }: TimelineDate): DayGroup | undefined {
|
||||
return this.#lookupCache[year]?.[month]?.[day];
|
||||
@@ -32,7 +33,8 @@ export class GroupInsertionCache {
|
||||
}
|
||||
|
||||
get updatedBuckets() {
|
||||
const updated = new SvelteSet<MonthGroup>();
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const updated = new Set<MonthGroup>();
|
||||
for (const group of this.changedDayGroups) {
|
||||
updated.add(group.monthGroup);
|
||||
}
|
||||
@@ -40,7 +42,8 @@ export class GroupInsertionCache {
|
||||
}
|
||||
|
||||
get bucketsWithNewDayGroups() {
|
||||
const updated = new SvelteSet<MonthGroup>();
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const updated = new Set<MonthGroup>();
|
||||
for (const group of this.newDayGroups) {
|
||||
updated.add(group.monthGroup);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function loadFromTimeBuckets(
|
||||
}
|
||||
}
|
||||
|
||||
const unprocessedAssets = monthGroup.addAssets(bucketResponse);
|
||||
const unprocessedAssets = monthGroup.addAssets(bucketResponse, true);
|
||||
if (unprocessedAssets.length > 0) {
|
||||
console.error(
|
||||
`Warning: getTimeBucket API returning assets not in requested month: ${monthGroup.yearMonth.month}, ${JSON.stringify(
|
||||
|
||||
@@ -153,7 +153,7 @@ export class MonthGroup {
|
||||
};
|
||||
}
|
||||
|
||||
addAssets(bucketAssets: TimeBucketAssetResponseDto) {
|
||||
addAssets(bucketAssets: TimeBucketAssetResponseDto, preSorted: boolean) {
|
||||
const addContext = new GroupInsertionCache();
|
||||
for (let i = 0; i < bucketAssets.id.length; i++) {
|
||||
const { localDateTime, fileCreatedAt } = getTimes(
|
||||
@@ -194,6 +194,9 @@ export class MonthGroup {
|
||||
}
|
||||
this.addTimelineAsset(timelineAsset, addContext);
|
||||
}
|
||||
if (preSorted) {
|
||||
return addContext.unprocessedAssets;
|
||||
}
|
||||
|
||||
for (const group of addContext.existingDayGroups) {
|
||||
group.sortAssets(this.#sortOrder);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { type ServerAboutResponseDto } from '@immich/sdk';
|
||||
import { Icon, Modal, ModalBody } from '@immich/ui';
|
||||
import { mdiBugOutline, mdiFaceAgent, mdiGit, mdiGithub, mdiInformationOutline } from '@mdi/js';
|
||||
import { siDiscord } from 'simple-icons';
|
||||
import { type SimpleIcon, siDiscord } from 'simple-icons';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
@@ -13,94 +13,57 @@
|
||||
let { onClose, info }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#snippet link(url: string, icon: string | SimpleIcon, text: string)}
|
||||
<div>
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
<Icon {icon} size="1.5em" class="inline-block" />
|
||||
<p class="font-medium text-primary text-sm underline inline-block">
|
||||
{text}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<Modal title={$t('support_and_feedback')} {onClose} size="small">
|
||||
<ModalBody>
|
||||
<p>{$t('official_immich_resources')}</p>
|
||||
<div class="flex flex-col sm:grid sm:grid-cols-2 gap-2 mt-5">
|
||||
<div>
|
||||
<a href="https://docs.{info.version}.archive.immich.app/overview/introduction" target="_blank" rel="noreferrer">
|
||||
<Icon icon={mdiInformationOutline} size="1.5em" class="inline-block" />
|
||||
<p class="font-medium text-primary text-sm underline inline-block" id="documentation-label">
|
||||
{$t('documentation')}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 mt-5">
|
||||
{@render link(
|
||||
`https://docs.${info.version}.archive.immich.app/overview/introduction`,
|
||||
mdiInformationOutline,
|
||||
$t('documentation'),
|
||||
)}
|
||||
|
||||
<div>
|
||||
<a href="https://github.com/immich-app/immich/" target="_blank" rel="noreferrer">
|
||||
<Icon icon={mdiGithub} size="1.5em" class="inline-block" />
|
||||
<p class="font-medium text-primary text-sm underline inline-block" id="github-label">
|
||||
{$t('source')}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
{@render link('https://github.com/immich-app/immich/', mdiGithub, $t('source'))}
|
||||
|
||||
<div>
|
||||
<a href="https://discord.immich.app" target="_blank" rel="noreferrer">
|
||||
<Icon icon={siDiscord} class="inline-block" size="1.5em" />
|
||||
<p class="font-medium text-primary text-sm underline inline-block" id="github-label">
|
||||
{$t('discord')}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
{@render link('https://discord.immich.app', siDiscord, $t('discord'))}
|
||||
|
||||
<div>
|
||||
<a href="https://github.com/immich-app/immich/issues/new/choose" target="_blank" rel="noreferrer">
|
||||
<Icon icon={mdiBugOutline} size="1.5em" class="inline-block" />
|
||||
<p class="font-medium text-primary text-sm underline inline-block" id="github-label">
|
||||
{$t('bugs_and_feature_requests')}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
{@render link(
|
||||
'https://github.com/immich-app/immich/issues/new/choose',
|
||||
mdiBugOutline,
|
||||
$t('bugs_and_feature_requests'),
|
||||
)}
|
||||
</div>
|
||||
{#if info.thirdPartyBugFeatureUrl || info.thirdPartySourceUrl || info.thirdPartyDocumentationUrl || info.thirdPartySupportUrl}
|
||||
<p class="mt-5">{$t('third_party_resources')}</p>
|
||||
<p class="text-sm mt-1">
|
||||
{$t('support_third_party_description')}
|
||||
</p>
|
||||
<div class="flex flex-col sm:grid sm:grid-cols-2 gap-2 mt-5">
|
||||
<div class="flex flex-col gap-2 mt-5">
|
||||
{#if info.thirdPartyDocumentationUrl}
|
||||
<div>
|
||||
<a href={info.thirdPartyDocumentationUrl} target="_blank" rel="noreferrer">
|
||||
<Icon icon={mdiInformationOutline} size="1.5em" class="inline-block" />
|
||||
<p class="font-medium text-primary text-sm underline inline-block" id="documentation-label">
|
||||
{$t('documentation')}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
{@render link(info.thirdPartyDocumentationUrl, mdiInformationOutline, $t('documentation'))}
|
||||
{/if}
|
||||
|
||||
{#if info.thirdPartySourceUrl}
|
||||
<div>
|
||||
<a href={info.thirdPartySourceUrl} target="_blank" rel="noreferrer">
|
||||
<Icon icon={mdiGit} size="1.5em" class="inline-block" />
|
||||
<p class="font-medium text-primary text-sm underline inline-block" id="github-label">
|
||||
{$t('source')}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
{@render link(info.thirdPartySourceUrl, mdiGit, $t('source'))}
|
||||
{/if}
|
||||
|
||||
{#if info.thirdPartySupportUrl}
|
||||
<div>
|
||||
<a href={info.thirdPartySupportUrl} target="_blank" rel="noreferrer">
|
||||
<Icon icon={mdiFaceAgent} class="inline-block" size="1.5em" />
|
||||
<p class="font-medium text-primary text-sm underline inline-block" id="github-label">
|
||||
{$t('support')}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
{@render link(info.thirdPartySupportUrl, mdiFaceAgent, $t('support'))}
|
||||
{/if}
|
||||
|
||||
{#if info.thirdPartyBugFeatureUrl}
|
||||
<div>
|
||||
<a href={info.thirdPartyBugFeatureUrl} target="_blank" rel="noreferrer">
|
||||
<Icon icon={mdiBugOutline} size="1.5em" class="inline-block" />
|
||||
<p class="font-medium text-primary text-sm underline inline-block" id="github-label">
|
||||
{$t('bugs_and_feature_requests')}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
{@render link(info.thirdPartyBugFeatureUrl, mdiBugOutline, $t('bugs_and_feature_requests'))}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user