merge main

This commit is contained in:
Alex Tran
2023-11-28 22:53:39 -06:00
205 changed files with 3298 additions and 2190 deletions
@@ -9,8 +9,11 @@
const restoreUser = async () => {
const restoredUser = await api.userApi.restoreUser({ id: user.id });
if (restoredUser.data.deletedAt == null) dispatch('user-restore-success');
else dispatch('user-restore-fail');
if (restoredUser.data.deletedAt == null) {
dispatch('user-restore-success');
} else {
dispatch('user-restore-fail');
}
};
</script>
@@ -21,7 +21,8 @@
return '0'.repeat(zeroLength);
};
$: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, 0);
const TiB = 1024 ** 4;
$: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, stats.usage > TiB ? 2 : 0);
</script>
<div class="flex flex-col gap-5">
@@ -16,8 +16,18 @@
let savedConfig: SystemConfigJobDto;
let defaultConfig: SystemConfigJobDto;
const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[];
const jobNames = Object.values(JobName).filter((jobName) => !ignoredJobs.includes(jobName as JobName));
const jobNames = [
JobName.ThumbnailGeneration,
JobName.MetadataExtraction,
JobName.Library,
JobName.Sidecar,
JobName.ObjectTagging,
JobName.ClipEncoding,
JobName.RecognizeFaces,
JobName.VideoConversion,
JobName.StorageTemplateMigration,
JobName.Migration,
];
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
@@ -4,13 +4,12 @@
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { handleError } from '$lib/utils/handle-error';
import { api, CitiesFile, SystemConfigDto } from '@api';
import { api, SystemConfigDto } from '@api';
import { cloneDeep, isEqual } from 'lodash-es';
import { fade } from 'svelte/transition';
import SettingAccordion from '../setting-accordion.svelte';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SettingSelect from '../setting-select.svelte';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
export let config: SystemConfigDto; // this is the config that is being edited
@@ -39,7 +38,6 @@
},
reverseGeocoding: {
enabled: config.reverseGeocoding.enabled,
citiesFileOverride: config.reverseGeocoding.citiesFileOverride,
},
},
});
@@ -131,24 +129,6 @@
subtitle="Enable reverse geocoding"
bind:checked={config.reverseGeocoding.enabled}
/>
<hr />
<SettingSelect
label="Precision"
desc="Set reverse geocoding precision"
name="reverse-geocoding-precision"
bind:value={config.reverseGeocoding.citiesFileOverride}
options={[
{ value: CitiesFile.Cities500, text: 'Cities with more than 500 people' },
{ value: CitiesFile.Cities1000, text: 'Cities with more than 1000 people' },
{ value: CitiesFile.Cities5000, text: 'Cities with more than 5000 people' },
{ value: CitiesFile.Cities15000, text: 'Cities with more than 15000 people' },
]}
disabled={disabled || !config.reverseGeocoding.enabled}
isEdited={config.reverseGeocoding.citiesFileOverride !==
savedConfig.reverseGeocoding.citiesFileOverride}
/>
</div></SettingAccordion
>
@@ -44,14 +44,10 @@
>
<span class="flex gap-1 text-sm">
{#if variant === 'simple'}
<span
>{#if album.shared}Shared{/if}
</span>
<span>{album.shared ? 'Shared' : ''}</span>
{:else}
<span>{album.assetCount} items</span>
<span
>{#if album.shared} · Shared{/if}
</span>
<span>{album.shared ? ' · Shared' : ''} </span>
{/if}
</span>
</div>
@@ -17,12 +17,14 @@
export let circle = false;
export let hidden = false;
export let border = false;
export let preload = true;
let complete = false;
export let eyeColor: 'black' | 'white' = 'white';
</script>
<img
loading={preload ? 'eager' : 'lazy'}
style:width={widthStyle}
style:height={heightStyle}
style:filter={hidden ? 'grayscale(50%)' : 'none'}
@@ -5,10 +5,10 @@
export let option: Sort;
const handleSort = () => {
if (albumViewSettings === option.sortTitle) {
if (albumViewSettings === option.title) {
option.sortDesc = !option.sortDesc;
} else {
albumViewSettings = option.sortTitle;
albumViewSettings = option.title;
}
};
</script>
@@ -18,12 +18,12 @@
class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
on:click={() => handleSort()}
>
{#if albumViewSettings === option.sortTitle}
{#if albumViewSettings === option.title}
{#if option.sortDesc}
&#8595;
{:else}
&#8593;
{/if}
{/if}{option.table}</button
{/if}{option.title}</button
></th
>
@@ -12,15 +12,21 @@
import { handleError } from '$lib/utils/handle-error';
import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
import { mdiCallMerge, mdiClose, mdiMagnify, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { cloneDeep } from 'lodash-es';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
export let person: PersonResponseDto;
let people: PersonResponseDto[] = [];
let peopleCopy: PersonResponseDto[] = [];
let selectedPeople: PersonResponseDto[] = [];
let screenHeight: number;
let isShowConfirmation = false;
let name = '';
let searchWord: string;
let isSearchingPeople = false;
let dispatch = createEventDispatcher();
$: hasSelection = selectedPeople.length > 0;
@@ -31,12 +37,49 @@
onMount(async () => {
const { data } = await api.personApi.getAllPeople({ withHidden: false });
people = data.people;
peopleCopy = cloneDeep(people);
});
const onClose = () => {
dispatch('go-back');
};
const resetSearch = () => {
name = '';
people = peopleCopy;
};
const searchPeople = async (force: boolean) => {
if (name === '') {
people = peopleCopy;
return;
}
if (!force) {
if (people.length < 20 && name.startsWith(searchWord)) {
people = peopleCopy
.filter((person: PersonResponseDto) => {
const nameParts = person.name.split(' ');
return nameParts.some((splitName) => splitName.toLowerCase().startsWith(name.toLowerCase()));
})
.slice(0, 10);
return;
}
}
const timeout = setTimeout(() => (isSearchingPeople = true), 100);
try {
const { data } = await api.searchApi.searchPerson({ name });
people = data;
searchWord = name;
} catch (error) {
handleError(error, "Can't search people");
} finally {
clearTimeout(timeout);
}
isSearchingPeople = false;
};
const handleSwapPeople = () => {
[person, selectedPeople[0]] = [selectedPeople[0], person];
goto(`${AppRoute.PEOPLE}/${person.id}?action=merge`);
@@ -136,9 +179,39 @@
<FaceThumbnail {person} border circle selectable={false} thumbnailSize={180} />
</div>
</div>
<div
class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 p-10 dark:bg-immich-dark-gray"
style:max-height={screenHeight - 200 - 200 + 'px'}
class="flex w-40 sm:w-48 md:w-96 h-14 rounded-lg bg-gray-100 p-2 dark:bg-gray-700 mb-8 gap-2 place-items-center"
>
<button on:click={() => searchPeople(true)}>
<div class="w-fit">
<Icon path={mdiMagnify} size="24" />
</div>
</button>
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
class="w-full gap-2 bg-gray-100 dark:bg-gray-700 dark:text-white"
type="text"
placeholder="Search names"
bind:value={name}
on:input={() => searchPeople(false)}
/>
{#if name}
<button on:click={resetSearch}>
<Icon path={mdiClose} />
</button>
{/if}
{#if isSearchingPeople}
<div class="flex place-items-center">
<LoadingSpinner />
</div>
{/if}
</div>
<div
class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 pt-8 px-8 pb-10 dark:bg-immich-dark-gray"
style:max-height={screenHeight - 250 - 250 + 'px'}
>
<div class="grid-col-2 grid gap-8 md:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10">
{#each unselectedPeople as person (person.id)}
@@ -12,6 +12,7 @@
import Icon from '$lib/components/elements/icon.svelte';
export let person: PersonResponseDto;
export let preload = false;
type MenuItemEvent = 'change-name' | 'set-birth-date' | 'merge-faces' | 'hide-face';
let dispatch = createEventDispatcher<{
@@ -48,6 +49,7 @@
<div class="h-48 w-48 rounded-xl brightness-95 filter">
<ImageThumbnail
shadow
{preload}
url={api.getPeopleThumbnailUrl(person.id)}
altText={person.name}
title={person.name}
@@ -275,6 +275,8 @@
<style>
.main-view {
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.3), 0 8px 12px 6px rgba(0, 0, 0, 0.15);
box-shadow:
0 4px 4px 0 rgba(0, 0, 0, 0.3),
0 8px 12px 6px rgba(0, 0, 0, 0.15);
}
</style>
@@ -91,6 +91,8 @@
<style>
.memory-card {
box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 1px 3px 1px;
box-shadow:
rgba(60, 64, 67, 0.3) 0px 1px 2px 0px,
rgba(60, 64, 67, 0.15) 0px 1px 3px 1px;
}
</style>
@@ -52,7 +52,7 @@
<svelte:fragment slot="title">
<span class="flex place-items-center gap-2">
<p class="font-medium">
Add to {#if shared}Shared {/if} Album
Add to {shared ? 'Shared ' : ''}Album
</p>
</span>
</svelte:fragment>
@@ -88,7 +88,7 @@
<Icon path={mdiPlus} size="30" />
</div>
<p class="">
New {#if shared}Shared {/if}Album {#if search.length > 0}<b>{search}</b>{/if}
New {shared ? 'Shared ' : ''}Album {#if search.length > 0}<b>{search}</b>{/if}
</p>
</button>
{#if filteredAlbums.length > 0}
@@ -101,7 +101,8 @@
{#if !shared}
<p class="px-5 py-3 text-xs">
{#if search.length === 0}ALL {/if}ALBUMS
{#if search.length === 0}ALL
{/if}ALBUMS
</p>
{/if}
{#each filteredAlbums as album (album.id)}
@@ -111,10 +111,20 @@
</script>
{#await style then style}
<MapLibre {style} class="h-full" {center} {zoom} attributionControl={false} diffStyleUpdates={true} let:map bind:map>
<MapLibre
{style}
class="h-full"
{center}
{zoom}
attributionControl={false}
diffStyleUpdates={true}
let:map
on:load={(event) => event.detail.setMaxZoom(14)}
bind:map
>
<NavigationControl position="top-left" showCompass={!simplified} />
{#if !simplified}
<GeolocateControl position="top-left" fitBoundsOptions={{ maxZoom: 12 }} />
<GeolocateControl position="top-left" />
<FullscreenControl position="top-left" />
<ScaleControl />
<AttributionControl compact={false} />
@@ -134,7 +144,7 @@
}),
}}
id="geojson"
cluster={{ maxZoom: 14, radius: 500 }}
cluster={{ radius: 500 }}
>
<MarkerLayer
applyToClusters
@@ -66,13 +66,17 @@
<div class="h-[15px] rounded-md bg-immich-warning transition-all" style="width: 100%" />
<p class="absolute top-0 h-full w-full text-center text-[10px]">
Skipped
{#if uploadAsset.message} ({uploadAsset.message}){/if}
{#if uploadAsset.message}
({uploadAsset.message})
{/if}
</p>
{:else if uploadAsset.state === UploadState.DONE}
<div class="h-[15px] rounded-md bg-immich-success transition-all" style="width: 100%" />
<p class="absolute top-0 h-full w-full text-center text-[10px]">
Uploaded
{#if uploadAsset.message} ({uploadAsset.message}){/if}
{#if uploadAsset.message}
({uploadAsset.message})
{/if}
</p>
{/if}
</div>
@@ -7,13 +7,14 @@
import { createEventDispatcher } from 'svelte';
import { goto } from '$app/navigation';
import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js';
import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
export let link: SharedLinkResponseDto;
let expirationCountdown: luxon.DurationObjectUnits;
const dispatch = createEventDispatcher();
const getAssetInfo = async (): Promise<AssetResponseDto> => {
const getThumbnail = async (): Promise<AssetResponseDto> => {
let assetId = '';
if (link.album?.albumThumbnailAssetId) {
@@ -60,18 +61,28 @@
class="flex w-full gap-4 border-b border-gray-200 py-4 transition-all hover:border-immich-primary dark:border-gray-600 dark:text-immich-gray dark:hover:border-immich-dark-primary"
>
<div>
{#await getAssetInfo()}
<LoadingSpinner />
{:then asset}
{#if link?.album?.albumThumbnailAssetId || link.assets.length > 0}
{#await getThumbnail()}
<LoadingSpinner />
{:then asset}
<img
id={asset.id}
src={api.getAssetThumbnailUrl(asset.id, ThumbnailFormat.Webp)}
alt={asset.id}
class="h-[100px] w-[100px] rounded-lg object-cover"
loading="lazy"
draggable="false"
/>
{/await}
{:else}
<img
id={asset.id}
src={api.getAssetThumbnailUrl(asset.id, ThumbnailFormat.Webp)}
alt={asset.id}
src={noThumbnailUrl}
alt={'Album without assets'}
class="h-[100px] w-[100px] rounded-lg object-cover"
loading="lazy"
draggable="false"
/>
{/await}
{/if}
</div>
<div class="flex flex-col justify-between">