feat: library details page (#23908)

* feat: library details page

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
Daniel Dietzler
2025-11-18 21:27:41 +01:00
committed by GitHub
parent c086a65fa8
commit d310c6f3cd
29 changed files with 814 additions and 863 deletions

View File

@@ -0,0 +1,43 @@
<script lang="ts">
import { handleAddLibraryExclusionPattern } from '$lib/services/library.service';
import type { LibraryResponseDto } from '@immich/sdk';
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, Text } from '@immich/ui';
import { mdiFolderSync } from '@mdi/js';
import { t } from 'svelte-i18n';
type Props = {
library: LibraryResponseDto;
onClose: () => void;
};
const { library, onClose }: Props = $props();
let exclusionPattern = $state('');
const onsubmit = async () => {
const success = await handleAddLibraryExclusionPattern(library, exclusionPattern);
if (success) {
onClose();
}
};
</script>
<Modal title={$t('add_exclusion_pattern')} icon={mdiFolderSync} {onClose} size="small">
<ModalBody>
<form {onsubmit} autocomplete="off" id="library-exclusion-pattern-form">
<Text size="small" class="mb-4">{$t('admin.exclusion_pattern_description')}</Text>
<Field label={$t('pattern')}>
<Input bind:value={exclusionPattern} />
</Field>
</form>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth form="library-exclusion-pattern-form">
{$t('add')}
</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -0,0 +1,45 @@
<script lang="ts">
import { handleEditExclusionPattern } from '$lib/services/library.service';
import type { LibraryResponseDto } from '@immich/sdk';
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, Text } from '@immich/ui';
import { mdiFolderSync } from '@mdi/js';
import { t } from 'svelte-i18n';
type Props = {
library: LibraryResponseDto;
exclusionPattern: string;
onClose: () => void;
};
const { library, exclusionPattern, onClose }: Props = $props();
let newExclusionPattern = $state(exclusionPattern);
const onsubmit = async () => {
const success = await handleEditExclusionPattern(library, exclusionPattern, newExclusionPattern);
if (success) {
onClose();
}
};
</script>
<Modal title={$t('edit_exclusion_pattern')} icon={mdiFolderSync} {onClose} size="small">
<ModalBody>
<form {onsubmit} autocomplete="off" id="library-exclusion-pattern-form">
<Text size="small" class="mb-4">{$t('admin.exclusion_pattern_description')}</Text>
<Field label={$t('pattern')}>
<Input bind:value={newExclusionPattern} />
</Field>
</form>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth form="library-exclusion-pattern-form">
{$t('save')}
</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -1,78 +0,0 @@
<script lang="ts">
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiFolderRemove } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
interface Props {
exclusionPattern: string;
exclusionPatterns?: string[];
isEditing?: boolean;
submitText?: string;
onClose: (data?: { action: 'delete' } | { action: 'submit'; exclusionPattern: string }) => void;
}
let {
exclusionPattern = $bindable(),
exclusionPatterns = $bindable([]),
isEditing = false,
submitText = $t('submit'),
onClose,
}: Props = $props();
onMount(() => {
if (isEditing) {
exclusionPatterns = exclusionPatterns.filter((pattern) => pattern !== exclusionPattern);
}
});
let isDuplicate = $derived(exclusionPattern !== null && exclusionPatterns.includes(exclusionPattern));
let canSubmit = $derived(exclusionPattern && !exclusionPatterns.includes(exclusionPattern));
const onsubmit = (event: Event) => {
event.preventDefault();
if (canSubmit) {
onClose({ action: 'submit', exclusionPattern });
}
};
</script>
<Modal size="small" title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} {onClose}>
<ModalBody>
<form {onsubmit} autocomplete="off" id="add-exclusion-pattern-form">
<p class="py-5 text-sm">
{$t('admin.exclusion_pattern_description')}
<br /><br />
{$t('admin.add_exclusion_pattern_description')}
</p>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="exclusionPattern">{$t('pattern')}</label>
<input
class="immich-form-input"
id="exclusionPattern"
name="exclusionPattern"
type="text"
bind:value={exclusionPattern}
/>
</div>
<div class="mt-8 flex w-full gap-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">{$t('errors.exclusion_pattern_already_exists')}</p>
{/if}
</div>
</form>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
{#if isEditing}
<Button shape="round" color="danger" fullWidth onclick={() => onClose({ action: 'delete' })}
>{$t('delete')}</Button
>
{/if}
<Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="add-exclusion-pattern-form">
{submitText}
</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import { handleAddLibraryFolder } from '$lib/services/library.service';
import type { LibraryResponseDto } from '@immich/sdk';
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, Text } from '@immich/ui';
import { mdiFolderSync } from '@mdi/js';
import { t } from 'svelte-i18n';
type Props = {
library: LibraryResponseDto;
onClose: () => void;
};
const { library, onClose }: Props = $props();
let folder = $state('');
const onsubmit = async () => {
const success = await handleAddLibraryFolder(library, folder);
if (success) {
onClose();
}
};
</script>
<Modal title={$t('library_add_folder')} icon={mdiFolderSync} {onClose} size="small">
<ModalBody>
<form {onsubmit} autocomplete="off" id="library-import-path-form">
<Text size="small" class="mb-4">{$t('admin.library_folder_description')}</Text>
<Field label={$t('path')}>
<Input bind:value={folder} />
</Field>
</form>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth form="library-import-path-form">
{$t('add')}
</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -0,0 +1,45 @@
<script lang="ts">
import { handleEditLibraryFolder } from '$lib/services/library.service';
import type { LibraryResponseDto } from '@immich/sdk';
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, Text } from '@immich/ui';
import { mdiFolderSync } from '@mdi/js';
import { t } from 'svelte-i18n';
type Props = {
library: LibraryResponseDto;
folder: string;
onClose: () => void;
};
const { library, folder, onClose }: Props = $props();
let newFolder = $state(folder);
const onsubmit = async () => {
const success = await handleEditLibraryFolder(library, folder, newFolder);
if (success) {
onClose();
}
};
</script>
<Modal title={$t('library_edit_folder')} icon={mdiFolderSync} {onClose} size="small">
<ModalBody>
<form {onsubmit} autocomplete="off" id="library-import-path-form">
<Text size="small" class="mb-4">{$t('admin.library_folder_description')}</Text>
<Field label={$t('path')}>
<Input bind:value={newFolder} />
</Field>
</form>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth form="library-import-path-form">
{$t('save')}
</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -1,75 +0,0 @@
<script lang="ts">
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiFolderSync } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
interface Props {
importPath: string | null;
importPaths?: string[];
title?: string;
cancelText?: string;
submitText?: string;
isEditing?: boolean;
onClose: (data?: { action: 'delete' } | { action: 'submit'; importPath: string | null }) => void;
}
let {
importPath = $bindable(),
importPaths = $bindable([]),
title = $t('import_path'),
cancelText = $t('cancel'),
submitText = $t('save'),
isEditing = false,
onClose,
}: Props = $props();
onMount(() => {
if (isEditing) {
importPaths = importPaths.filter((path) => path !== importPath);
}
});
let isDuplicate = $derived(importPath !== null && importPaths.includes(importPath));
let canSubmit = $derived(importPath !== '' && importPath !== null && !importPaths.includes(importPath));
const onsubmit = (event: Event) => {
event.preventDefault();
if (canSubmit) {
onClose({ action: 'submit', importPath });
}
};
</script>
<Modal {title} icon={mdiFolderSync} {onClose} size="small">
<ModalBody>
<form {onsubmit} autocomplete="off" id="library-import-path-form">
<p class="py-5 text-sm">{$t('admin.library_import_path_description')}</p>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="path">{$t('path')}</label>
<input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
</div>
<div class="mt-8 flex w-full gap-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">{$t('errors.import_path_already_exists')}</p>
{/if}
</div>
</form>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{cancelText}</Button>
{#if isEditing}
<Button shape="round" color="danger" fullWidth onclick={() => onClose({ action: 'delete' })}>
{$t('delete')}
</Button>
{/if}
<Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="library-import-path-form">
{submitText}
</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -1,21 +1,25 @@
<script lang="ts">
import { handleRenameLibrary } from '$lib/services/library.service';
import type { LibraryResponseDto } from '@immich/sdk';
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiRenameOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
library: Partial<LibraryResponseDto>;
onClose: (library?: Partial<LibraryResponseDto>) => void;
}
type Props = {
library: LibraryResponseDto;
onClose: () => void;
};
let { library, onClose }: Props = $props();
let newName = $state(library.name);
const onsubmit = (event: Event) => {
event.preventDefault();
onClose({ ...library, name: newName });
const onsubmit = async () => {
const success = await handleRenameLibrary(library, newName);
if (success) {
onClose();
}
};
</script>