feat(server,web): libraries (#3124)

* feat: libraries

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Jonathan Jogenfors 2023-09-20 13:16:33 +02:00 committed by GitHub
parent 816db700e1
commit acdc66413c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
143 changed files with 10941 additions and 386 deletions

View file

@ -0,0 +1,55 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import FolderRemove from 'svelte-material-icons/FolderRemove.svelte';
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
export let exclusionPattern: string;
export let canDelete = false;
export let submitText = 'Submit';
const dispatch = createEventDispatcher();
const handleCancel = () => dispatch('cancel');
const handleSubmit = () => dispatch('submit', { excludePattern: exclusionPattern });
</script>
<FullScreenModal on:clickOutside={() => handleCancel()}>
<div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<FolderRemove size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Add Exclusion pattern</h1>
</div>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<p class="p-5 text-sm">
Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have
folders that contain files you don't want to import, such as RAW files.
<br /><br />
Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named "Raw",
use "**/Raw/**". To ignore all files ending in ".tif", use "**/*.tif". To ignore an absolute path, use "/path/to/ignore".
</p>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="exclusionPattern">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 px-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
{#if canDelete}
<Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button>
{/if}
<Button type="submit" fullwidth>{submitText}</Button>
</div>
</form>
</div>
</FullScreenModal>

View file

@ -0,0 +1,53 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import FolderSync from 'svelte-material-icons/FolderSync.svelte';
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
export let importPath: string;
export let title = 'Import path';
export let cancelText = 'Cancel';
export let submitText = 'Save';
export let canDelete = false;
const dispatch = createEventDispatcher();
const handleCancel = () => dispatch('cancel');
const handleSubmit = () => dispatch('submit', { importPath });
</script>
<FullScreenModal on:clickOutside={() => handleCancel()}>
<div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<FolderSync size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
{title}
</h1>
</div>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<p class="p-5 text-sm">
Specify a folder to import. This folder, including subfolders, will be scanned for images and videos. Note that
you are only allowed to import paths inside of your account's external path, configured in the administrative
settings.
</p>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="path">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 px-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>{cancelText}</Button>
{#if canDelete}
<Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button>
{/if}
<Button type="submit" fullwidth>{submitText}</Button>
</div>
</form>
</div>
</FullScreenModal>

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import { handleError } from '../../utils/handle-error';
import LibraryImportPathForm from './library-import-path-form.svelte';
import { onMount } from 'svelte';
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
import type { LibraryResponseDto } from '@api';
export let library: Partial<LibraryResponseDto>;
let addImportPath = false;
let editImportPath: number | null = null;
let importPathToAdd: string;
let editedImportPath: string;
let importPaths: string[] = [];
onMount(() => {
if (library.importPaths) {
importPaths = library.importPaths;
} else {
library.importPaths = [];
}
});
const dispatch = createEventDispatcher();
const handleCancel = () => {
dispatch('cancel');
};
const handleSubmit = () => {
dispatch('submit', { ...library });
};
const handleAddImportPath = async () => {
if (!addImportPath) {
return;
}
if (!library.importPaths) {
library.importPaths = [];
}
try {
library.importPaths.push(importPathToAdd);
importPaths = library.importPaths;
} catch (error) {
handleError(error, 'Unable to remove import path');
} finally {
addImportPath = false;
}
};
const handleEditImportPath = async () => {
if (editImportPath === null) {
return;
}
if (!library.importPaths) {
library.importPaths = [];
}
try {
library.importPaths[editImportPath] = editedImportPath;
importPaths = library.importPaths;
} catch (error) {
editImportPath = null;
handleError(error, 'Unable to edit import path');
} finally {
editImportPath = null;
}
};
const handleDeleteImportPath = async () => {
if (editImportPath === null) {
return;
}
try {
if (!library.importPaths) {
library.importPaths = [];
}
const pathToDelete = library.importPaths[editImportPath];
library.importPaths = library.importPaths.filter((path) => path != pathToDelete);
importPaths = library.importPaths;
} catch (error) {
handleError(error, 'Unable to delete import path');
} finally {
editImportPath = null;
}
};
</script>
{#if addImportPath}
<LibraryImportPathForm
title="Add Import Path"
submitText="Add"
bind:importPath={importPathToAdd}
on:submit={handleAddImportPath}
on:cancel={() => {
addImportPath = false;
}}
/>
{/if}
{#if editImportPath != null}
<LibraryImportPathForm
title="Edit Import Path"
submitText="Save"
canDelete={true}
bind:importPath={editedImportPath}
on:submit={handleEditImportPath}
on:delete={handleDeleteImportPath}
on:cancel={() => {
editImportPath = null;
}}
/>
{/if}
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-4">
<table class="text-left">
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
{#each importPaths as importPath, listIndex}
<tr
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
listIndex % 2 == 0
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
}`}
>
<td class="w-4/5 text-ellipsis px-4 text-sm">{importPath}</td>
<td class="w-1/5 text-ellipsis px-4 text-sm">
<button
type="button"
on:click={() => {
editImportPath = listIndex;
editedImportPath = importPath;
}}
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
>
<PencilOutline size="16" />
</button>
</td>
</tr>
{/each}
<tr
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
importPaths.length % 2 == 0
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
}`}
>
<td class="w-4/5 text-ellipsis px-4 text-sm" />
<td class="w-1/5 text-ellipsis px-4 text-sm"
><Button
type="button"
size="sm"
on:click={() => {
addImportPath = true;
}}>Add path</Button
></td
></tr
>
</tbody>
</table>
<div class="flex w-full justify-end gap-2">
<Button size="sm" color="gray" on:click={() => handleCancel()}>Cancel</Button>
<Button size="sm" type="submit">Save</Button>
</div>
</form>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import type { LibraryResponseDto } from '@api';
export let library: Partial<LibraryResponseDto>;
const dispatch = createEventDispatcher();
const handleCancel = () => {
dispatch('cancel');
};
const handleSubmit = () => {
dispatch('submit', { ...library });
};
</script>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-2">
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="path">Name</label>
<input class="immich-form-input" id="name" name="name" type="text" bind:value={library.name} />
</div>
<div class="flex w-full justify-end gap-2 pt-2">
<Button size="sm" color="gray" on:click={() => handleCancel()}>Cancel</Button>
<Button size="sm" type="submit">Save</Button>
</div>
</form>

View file

@ -0,0 +1,175 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import { LibraryType, type LibraryResponseDto } from '@api';
import { handleError } from '../../utils/handle-error';
import { onMount } from 'svelte';
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
import LibraryExclusionPatternForm from './library-exclusion-pattern-form.svelte';
export let library: Partial<LibraryResponseDto>;
let addExclusionPattern = false;
let editExclusionPattern: number | null = null;
let exclusionPatternToAdd: string;
let editedExclusionPattern: string;
let exclusionPatterns: string[] = [];
onMount(() => {
if (library.exclusionPatterns) {
exclusionPatterns = library.exclusionPatterns;
} else {
library.exclusionPatterns = [];
}
});
const dispatch = createEventDispatcher();
const handleCancel = () => {
dispatch('cancel');
};
const handleSubmit = () => {
dispatch('submit', { ...library, libraryType: LibraryType.External });
};
const handleAddExclusionPattern = async () => {
if (!addExclusionPattern) {
return;
}
if (!library.exclusionPatterns) {
library.exclusionPatterns = [];
}
try {
library.exclusionPatterns.push(exclusionPatternToAdd);
exclusionPatternToAdd = '';
exclusionPatterns = library.exclusionPatterns;
addExclusionPattern = false;
} catch (error) {
handleError(error, 'Unable to add exclude pattern');
}
};
const handleEditExclusionPattern = async () => {
if (editExclusionPattern === null) {
return;
}
if (!library.exclusionPatterns) {
library.exclusionPatterns = [];
}
try {
library.exclusionPatterns[editExclusionPattern] = editedExclusionPattern;
exclusionPatterns = library.exclusionPatterns;
} catch (error) {
handleError(error, 'Unable to edit exclude pattern');
} finally {
editExclusionPattern = null;
}
};
const handleDeleteExclusionPattern = async () => {
if (editExclusionPattern === null) {
return;
}
try {
if (!library.exclusionPatterns) {
library.exclusionPatterns = [];
}
const pathToDelete = library.exclusionPatterns[editExclusionPattern];
library.exclusionPatterns = library.exclusionPatterns.filter((path) => path != pathToDelete);
exclusionPatterns = library.exclusionPatterns;
} catch (error) {
handleError(error, 'Unable to delete exclude pattern');
} finally {
editExclusionPattern = null;
}
};
</script>
{#if addExclusionPattern}
<LibraryExclusionPatternForm
submitText="Add"
bind:exclusionPattern={exclusionPatternToAdd}
on:submit={handleAddExclusionPattern}
on:cancel={() => {
addExclusionPattern = false;
}}
/>
{/if}
{#if editExclusionPattern != null}
<LibraryExclusionPatternForm
submitText="Save"
canDelete={true}
bind:exclusionPattern={editedExclusionPattern}
on:submit={handleEditExclusionPattern}
on:delete={handleDeleteExclusionPattern}
on:cancel={() => {
editExclusionPattern = null;
}}
/>
{/if}
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-4">
<table class="w-full text-left">
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
{#each exclusionPatterns as exclusionPattern, listIndex}
<tr
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
listIndex % 2 == 0
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
}`}
>
<td class="w-3/4 text-ellipsis px-4 text-sm">{exclusionPattern}</td>
<td class="w-1/4 text-ellipsis px-4 text-sm">
<button
type="button"
on:click={() => {
editExclusionPattern = listIndex;
editedExclusionPattern = exclusionPattern;
}}
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
>
<PencilOutline size="16" />
</button>
</td>
</tr>
{/each}
<tr
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
exclusionPatterns.length % 2 == 0
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
}`}
>
<td class="w-3/4 text-ellipsis px-4 text-sm">
{#if exclusionPatterns.length === 0}
No pattern added
{/if}
</td>
<td class="w-1/4 text-ellipsis px-4 text-sm"
><Button
size="sm"
on:click={() => {
addExclusionPattern = true;
}}>Add Exclusion Pattern</Button
></td
></tr
>
</tbody>
</table>
<div class="flex w-full justify-end gap-4">
<Button size="sm" color="gray" on:click={() => handleCancel()}>Cancel</Button>
<Button size="sm" type="submit">Save</Button>
</div>
</form>