mirror of
https://github.com/immich-app/immich.git
synced 2026-07-28 14:47:30 -07:00
chore(deps): update dependency eslint-plugin-unicorn to v70 - abandoned (#29684)
* chore(deps): update dependency eslint-plugin-unicorn to v70 * fix: linting --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
co-authored by
renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Daniel Dietzler
parent
8061a2e5ff
commit
df970da59e
+28
-3
@@ -42,7 +42,7 @@ export default typescriptEslint.config(
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
},
|
||||
ignores: ['**/service-worker/**'],
|
||||
// ignores: ['**/service-worker/**'],
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
@@ -116,7 +116,7 @@ export default typescriptEslint.config(
|
||||
'unicorn/no-useless-undefined': 'off',
|
||||
'unicorn/prefer-spread': 'off',
|
||||
'unicorn/no-null': 'off',
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/name-replacements': 'off',
|
||||
'unicorn/no-nested-ternary': 'off',
|
||||
'unicorn/consistent-function-scoping': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
@@ -124,12 +124,37 @@ export default typescriptEslint.config(
|
||||
'unicorn/import-style': 'off',
|
||||
'unicorn/no-array-sort': 'off',
|
||||
'unicorn/no-for-loop': 'off',
|
||||
'svelte/button-has-type': 'error',
|
||||
'unicorn/no-unreadable-for-of-expression': 'off',
|
||||
'unicorn/no-break-in-nested-loop': 'off',
|
||||
'unicorn/no-top-level-assignment-in-function': 'off',
|
||||
'unicorn/prefer-uint8array-base64': 'off',
|
||||
'unicorn/max-nested-calls': 'off',
|
||||
'unicorn/no-declarations-before-early-exit': 'off',
|
||||
'unicorn/no-unreadable-object-destructuring': 'off',
|
||||
// not yet compatible with all our supported browsers
|
||||
'unicorn/prefer-promise-with-resolvers': 'off',
|
||||
// not yet compatible with all our supported browsers
|
||||
'unicorn/prefer-iterator-to-array': 'off',
|
||||
// not yet compatible with all our supported browsers
|
||||
'unicorn/prefer-array-from-async': 'off',
|
||||
// maybe we do want to enable this later. TBD
|
||||
'unicorn/prefer-await': 'off',
|
||||
'unicorn/consistent-class-member-order': 'off',
|
||||
'unicorn/class-reference-in-static-methods': ['error', { preferThis: false, preferSuper: false }],
|
||||
'unicorn/no-unsafe-property-key': 'off',
|
||||
'unicorn/consistent-boolean-name': 'off',
|
||||
'unicorn/no-non-function-verb-prefix': 'off',
|
||||
'unicorn/prefer-minimal-ternary': 'off',
|
||||
'unicorn/no-empty-file': 'off',
|
||||
// prefer the typescript-eslint type-aware version
|
||||
'unicorn/require-array-sort-compare': 'off',
|
||||
'@typescript-eslint/require-array-sort-compare': 'error',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
'@typescript-eslint/require-await': 'error',
|
||||
'@typescript-eslint/switch-exhaustiveness-check': ['error', { considerDefaultExhaustiveForUnions: true }],
|
||||
'svelte/button-has-type': 'error',
|
||||
'object-shorthand': ['error', 'always'],
|
||||
'svelte/no-navigation-without-resolve': 'off',
|
||||
},
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@
|
||||
"eslint-plugin-better-tailwindcss": "^4.5.0",
|
||||
"eslint-plugin-compat": "^7.0.0",
|
||||
"eslint-plugin-svelte": "^3.12.4",
|
||||
"eslint-plugin-unicorn": "^64.0.0",
|
||||
"eslint-plugin-unicorn": "^70.0.0",
|
||||
"factory.ts": "^1.4.1",
|
||||
"globals": "^17.0.0",
|
||||
"happy-dom": "^20.0.0",
|
||||
|
||||
@@ -6,7 +6,7 @@ import GoogleSansCode from '$lib/assets/fonts/GoogleSansCode/GoogleSansCode.ttf?
|
||||
export const handle = (async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
return html.replace('%app.font%', GoogleSans).replace('%app.monofont%', GoogleSansCode);
|
||||
return html.replace('%app.font%', () => GoogleSans).replace('%app.monofont%', () => GoogleSansCode);
|
||||
},
|
||||
});
|
||||
}) satisfies Handle;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const createObjectURLMock = vi.fn();
|
||||
|
||||
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
||||
Object.defineProperty(URL, 'createObjectURL', {
|
||||
writable: true,
|
||||
value: createObjectURLMock,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as sdk from '@immich/sdk';
|
||||
import type { Mock, MockedObject } from 'vitest';
|
||||
|
||||
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
||||
vi.mock('@immich/sdk', async (originalImport) => {
|
||||
const module = await originalImport<typeof import('@immich/sdk')>();
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ export function clickOutside(node: HTMLElement, options: Options = {}): ActionRe
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClick, false);
|
||||
node.addEventListener('keydown', handleKey, false);
|
||||
document.addEventListener('mousedown', handleClick, { capture: false });
|
||||
node.addEventListener('keydown', handleKey, { capture: false });
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
export interface DragAndDropOptions {
|
||||
index: number;
|
||||
onDragStart?: (index: number) => void;
|
||||
onDragEnter?: (index: number) => void;
|
||||
onDrop?: (e: DragEvent, index: number) => void;
|
||||
onDragEnd?: () => void;
|
||||
isDragging?: boolean;
|
||||
isDragOver?: boolean;
|
||||
}
|
||||
|
||||
export function dragAndDrop(node: HTMLElement, options: DragAndDropOptions) {
|
||||
let { index, onDragStart, onDragEnter, onDrop, onDragEnd, isDragging, isDragOver } = options;
|
||||
|
||||
const isFormElement = (element: HTMLElement) => {
|
||||
return element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT';
|
||||
};
|
||||
|
||||
const handleDragStart = (e: DragEvent) => {
|
||||
// Prevent drag if it originated from an input, textarea, or select element
|
||||
const target = e.target as HTMLElement;
|
||||
if (isFormElement(target)) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
onDragStart?.(index);
|
||||
};
|
||||
|
||||
const handleDragEnter = () => {
|
||||
onDragEnter?.(index);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
onDrop?.(e, index);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
onDragEnd?.();
|
||||
};
|
||||
|
||||
// Disable draggable when focusing on form elements (fixes Firefox input interaction)
|
||||
const handleFocusIn = (e: FocusEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (isFormElement(target)) {
|
||||
node.setAttribute('draggable', 'false');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocusOut = (e: FocusEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (isFormElement(target)) {
|
||||
node.setAttribute('draggable', 'true');
|
||||
}
|
||||
};
|
||||
|
||||
node.setAttribute('draggable', 'true');
|
||||
node.setAttribute('role', 'button');
|
||||
node.setAttribute('tabindex', '0');
|
||||
|
||||
node.addEventListener('dragstart', handleDragStart);
|
||||
node.addEventListener('dragenter', handleDragEnter);
|
||||
node.addEventListener('dragover', handleDragOver);
|
||||
node.addEventListener('drop', handleDrop);
|
||||
node.addEventListener('dragend', handleDragEnd);
|
||||
node.addEventListener('focusin', handleFocusIn);
|
||||
node.addEventListener('focusout', handleFocusOut);
|
||||
|
||||
// Update classes based on drag state
|
||||
const updateClasses = (dragging: boolean, dragOver: boolean) => {
|
||||
// Remove all drag-related classes first
|
||||
node.classList.remove('opacity-50', 'border-gray-400', 'dark:border-gray-500', 'border-solid');
|
||||
|
||||
// Add back only the active ones
|
||||
if (dragging) {
|
||||
node.classList.add('opacity-50');
|
||||
}
|
||||
|
||||
if (dragOver) {
|
||||
node.classList.add('border-gray-400', 'dark:border-gray-500', 'border-solid');
|
||||
node.classList.remove('border-transparent');
|
||||
} else {
|
||||
node.classList.add('border-transparent');
|
||||
}
|
||||
};
|
||||
|
||||
updateClasses(isDragging || false, isDragOver || false);
|
||||
|
||||
return {
|
||||
update(newOptions: DragAndDropOptions) {
|
||||
index = newOptions.index;
|
||||
onDragStart = newOptions.onDragStart;
|
||||
onDragEnter = newOptions.onDragEnter;
|
||||
onDrop = newOptions.onDrop;
|
||||
onDragEnd = newOptions.onDragEnd;
|
||||
|
||||
const newIsDragging = newOptions.isDragging || false;
|
||||
const newIsDragOver = newOptions.isDragOver || false;
|
||||
|
||||
if (newIsDragging !== isDragging || newIsDragOver !== isDragOver) {
|
||||
isDragging = newIsDragging;
|
||||
isDragOver = newIsDragOver;
|
||||
updateClasses(isDragging, isDragOver);
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
node.removeEventListener('dragstart', handleDragStart);
|
||||
node.removeEventListener('dragenter', handleDragEnter);
|
||||
node.removeEventListener('dragover', handleDragOver);
|
||||
node.removeEventListener('drop', handleDrop);
|
||||
node.removeEventListener('dragend', handleDragEnd);
|
||||
node.removeEventListener('focusin', handleFocusIn);
|
||||
node.removeEventListener('focusout', handleFocusOut);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -69,38 +69,44 @@ export function focusTrap(container: HTMLElement, options?: Options) {
|
||||
|
||||
// Add focus event listeners to sentinel nodes
|
||||
const handleStartFocus = () => {
|
||||
if (withDefaults(options).active) {
|
||||
const [, lastElement] = getFocusableElements();
|
||||
// If no elements, stay on backup sentinel
|
||||
if (lastElement) {
|
||||
lastElement.focus();
|
||||
} else {
|
||||
backupSentinel.focus();
|
||||
}
|
||||
if (!withDefaults(options).active) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, lastElement] = getFocusableElements();
|
||||
// If no elements, stay on backup sentinel
|
||||
if (lastElement) {
|
||||
lastElement.focus();
|
||||
} else {
|
||||
backupSentinel.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackupFocus = () => {
|
||||
// Backup sentinel keeps focus when there are no other focusable elements
|
||||
if (withDefaults(options).active) {
|
||||
const [firstElement] = getFocusableElements();
|
||||
// Only move focus if there are actual focusable elements
|
||||
if (firstElement) {
|
||||
firstElement.focus();
|
||||
}
|
||||
// Otherwise, focus stays on backup sentinel
|
||||
if (!withDefaults(options).active) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [firstElement] = getFocusableElements();
|
||||
// Only move focus if there are actual focusable elements
|
||||
if (firstElement) {
|
||||
firstElement.focus();
|
||||
}
|
||||
// Otherwise, focus stays on backup sentinel
|
||||
};
|
||||
|
||||
const handleEndFocus = () => {
|
||||
if (withDefaults(options).active) {
|
||||
const [firstElement] = getFocusableElements();
|
||||
// If no elements, move to backup sentinel
|
||||
if (firstElement) {
|
||||
firstElement.focus();
|
||||
} else {
|
||||
backupSentinel.focus();
|
||||
}
|
||||
if (!withDefaults(options).active) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [firstElement] = getFocusableElements();
|
||||
// If no elements, move to backup sentinel
|
||||
if (firstElement) {
|
||||
firstElement.focus();
|
||||
} else {
|
||||
backupSentinel.focus();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export function scrollMemory(
|
||||
const newScroll = sessionStorage.getItem(SessionStorageKey.SCROLL_POSITION);
|
||||
if (newScroll) {
|
||||
node.scroll({
|
||||
top: Number.parseFloat(newScroll),
|
||||
top: Number(newScroll),
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
@@ -71,10 +71,12 @@ export function scrollMemory(
|
||||
export function scrollMemoryClearer(_node: HTMLElement, { routeStartsWith, beforeClear }: Options) {
|
||||
const unsubscribeNavigating = navigating.subscribe((navigation) => {
|
||||
// Forget scroll position from main page if going somewhere else.
|
||||
if (navigation?.to && !navigation?.to.url.pathname.startsWith(routeStartsWith)) {
|
||||
beforeClear?.();
|
||||
sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
|
||||
if (!navigation?.to || navigation?.to.url.pathname.startsWith(routeStartsWith)) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeClear?.();
|
||||
sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
* https://github.com/hperrin/svelte-material-ui/blob/master/packages/common/src/internal/useActions.ts
|
||||
*/
|
||||
|
||||
export type SvelteActionReturnType<P> = {
|
||||
export type SvelteActionReturnType<P> = void | {
|
||||
update?: (newParams?: P) => void;
|
||||
destroy?: () => void;
|
||||
} | void;
|
||||
};
|
||||
|
||||
export type SvelteHTMLActionType<P> = (node: HTMLElement, params?: P) => SvelteActionReturnType<P>;
|
||||
|
||||
@@ -46,13 +46,15 @@ export function useActions(node: HTMLElement | SVGElement, actions: ActionArray)
|
||||
|
||||
if (actions) {
|
||||
for (const [i, returnEntry] of actionReturns.entries()) {
|
||||
if (returnEntry && returnEntry.update) {
|
||||
const actionEntry = actions[i];
|
||||
if (Array.isArray(actionEntry) && actionEntry.length > 1) {
|
||||
returnEntry.update(actionEntry[1]);
|
||||
} else {
|
||||
returnEntry.update();
|
||||
}
|
||||
if (!(returnEntry && returnEntry.update)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const actionEntry = actions[i];
|
||||
if (Array.isArray(actionEntry) && actionEntry.length > 1) {
|
||||
returnEntry.update(actionEntry[1]);
|
||||
} else {
|
||||
returnEntry.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function dragAndDrop(options: DragAndDropOptions): Attachment {
|
||||
const { index, onDragStart, onDragEnter, onDrop, onDragEnd, isDragging, isDragOver } = options;
|
||||
|
||||
const isFormElement = (el: HTMLElement) => {
|
||||
return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT';
|
||||
return ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName);
|
||||
};
|
||||
|
||||
const handleDragStart = (e: DragEvent) => {
|
||||
|
||||
@@ -232,7 +232,7 @@ export const getSettingsProvider = ($t: MessageFormatter) => {
|
||||
{
|
||||
title: $t('my_immich_title'),
|
||||
description: $t('my_immich_description'),
|
||||
onAction: () => copyToClipboard(getMyImmichLink().toString()),
|
||||
onAction: () => copyToClipboard(getMyImmichLink().href),
|
||||
shortcuts: { ctrl: true, shift: true, key: 'm' },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
const [group] = permission.split('.');
|
||||
if (!permissions[group]) {
|
||||
if (!Object.hasOwn(permissions, group)) {
|
||||
permissions[group] = [];
|
||||
}
|
||||
permissions[group].push(permission);
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
const events: EventMap<Events> = {};
|
||||
|
||||
for (const [name, listener] of Object.entries(props)) {
|
||||
if (listener) {
|
||||
const event = name.slice(2) as keyof Events;
|
||||
events[event] = listener as EventCallback<Events, typeof event>;
|
||||
if (!listener) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const event = name.slice(2) as keyof Events;
|
||||
events[event] = listener as EventCallback<Events, typeof event>;
|
||||
}
|
||||
|
||||
return assetViewerManager.on(events);
|
||||
|
||||
@@ -19,12 +19,14 @@
|
||||
let destroyed = false;
|
||||
|
||||
$effect(() => {
|
||||
if (src !== undefined && capturedSource === undefined) {
|
||||
capturedSource = src;
|
||||
untrack(() => {
|
||||
onStart?.();
|
||||
});
|
||||
if (src === undefined || capturedSource !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
capturedSource = src;
|
||||
untrack(() => {
|
||||
onStart?.();
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
const events: EventMap<Events> = {};
|
||||
|
||||
for (const [name, listener] of Object.entries(props)) {
|
||||
if (listener) {
|
||||
const event = name.slice(2) as keyof Events;
|
||||
events[event] = listener as EventCallback<Events, typeof event>;
|
||||
if (!listener) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const event = name.slice(2) as keyof Events;
|
||||
events[event] = listener as EventCallback<Events, typeof event>;
|
||||
}
|
||||
|
||||
return eventManager.on(events);
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
const label = $derived(schema.title ?? key);
|
||||
const description = $derived(schema.description);
|
||||
|
||||
const getValue = <T,>(defaultValue?: T) => (root === true ? config : (config?.[key] ?? defaultValue)) as T;
|
||||
const getValue = <T,>(defaultValue?: T) => (root ? config : (config?.[key] ?? defaultValue)) as T;
|
||||
const setValue = <T,>(value: T) => {
|
||||
if (root === true) {
|
||||
if (root) {
|
||||
config = value;
|
||||
} else {
|
||||
if (config === undefined) {
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
title={$t('admin.storage_template_enable_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.storageTemplate.enabled}
|
||||
isEdited={!(configToEdit.storageTemplate.enabled === config.storageTemplate.enabled)}
|
||||
isEdited={configToEdit.storageTemplate.enabled !== config.storageTemplate.enabled}
|
||||
/>
|
||||
|
||||
{#if !minified}
|
||||
@@ -137,9 +137,8 @@
|
||||
{disabled}
|
||||
subtitle={$t('admin.storage_template_hash_verification_enabled_description')}
|
||||
bind:checked={configToEdit.storageTemplate.hashVerificationEnabled}
|
||||
isEdited={!(
|
||||
configToEdit.storageTemplate.hashVerificationEnabled === config.storageTemplate.hashVerificationEnabled
|
||||
)}
|
||||
isEdited={configToEdit.storageTemplate.hashVerificationEnabled !==
|
||||
config.storageTemplate.hashVerificationEnabled}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -233,7 +232,7 @@
|
||||
required
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
bind:value={configToEdit.storageTemplate.template}
|
||||
isEdited={!(configToEdit.storageTemplate.template === config.storageTemplate.template)}
|
||||
isEdited={configToEdit.storageTemplate.template !== config.storageTemplate.template}
|
||||
/>
|
||||
|
||||
<div class="flex-0">
|
||||
|
||||
@@ -30,10 +30,12 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!assetViewerManager.isViewing && returnToMap) {
|
||||
returnToMap = false;
|
||||
void onClick();
|
||||
if (assetViewerManager.isViewing || !returnToMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
returnToMap = false;
|
||||
void onClick();
|
||||
});
|
||||
|
||||
const loadMapMarkers = async () => {
|
||||
|
||||
@@ -38,10 +38,12 @@
|
||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||
|
||||
dragAndDropFilesStore.subscribe((value) => {
|
||||
if (value.isDragging && value.files.length > 0) {
|
||||
handlePromiseError(fileUploadHandler({ files: value.files, albumId: album.id }));
|
||||
dragAndDropFilesStore.set({ isDragging: false, files: [] });
|
||||
if (!(value.isDragging && value.files.length > 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handlePromiseError(fileUploadHandler({ files: value.files, albumId: album.id }));
|
||||
dragAndDropFilesStore.set({ isDragging: false, files: [] });
|
||||
});
|
||||
|
||||
const handleStartSlideshow = async () => {
|
||||
|
||||
@@ -81,11 +81,8 @@
|
||||
// We make sure empty albums stay at the end of the list
|
||||
if (a === unknownYear) {
|
||||
return 1;
|
||||
} else if (b === unknownYear) {
|
||||
return -1;
|
||||
} else {
|
||||
return (Number.parseInt(a) - Number.parseInt(b)) * sortSign;
|
||||
}
|
||||
return b === unknownYear ? -1 : (Number.parseInt(a) - Number.parseInt(b)) * sortSign;
|
||||
});
|
||||
|
||||
return sortedByYear.map(([year, albums]) => ({
|
||||
@@ -106,13 +103,14 @@
|
||||
// of the list
|
||||
if (ownerIdA === currentUserId) {
|
||||
return -sortSign;
|
||||
} else if (ownerIdB === currentUserId) {
|
||||
return sortSign;
|
||||
} else {
|
||||
const ownerA = albumsA[0].albumUsers[0].user;
|
||||
const ownerB = albumsB[0].albumUsers[0].user;
|
||||
return ownerA.name.localeCompare(ownerB.name, $locale) * sortSign;
|
||||
}
|
||||
if (ownerIdB === currentUserId) {
|
||||
return sortSign;
|
||||
}
|
||||
|
||||
const ownerA = albumsA[0].albumUsers[0].user;
|
||||
const ownerB = albumsB[0].albumUsers[0].user;
|
||||
return ownerA.name.localeCompare(ownerB.name, $locale) * sortSign;
|
||||
});
|
||||
|
||||
return sortedByOwnerNames.map(([ownerId, albums]) => ({
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('AlbumCard component', () => {
|
||||
});
|
||||
|
||||
it('dispatches "onShowContextMenu" event on context menu click with mouse coordinates', async () => {
|
||||
const contextMenuButton = sut.getByTestId('context-button-parent').children[0];
|
||||
const contextMenuButton = sut.getByTestId('context-button-parent').firstElementChild!;
|
||||
expect(contextMenuButton).toBeDefined();
|
||||
|
||||
// Mock getBoundingClientRect to return a bounding rectangle that will result in the expected position
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
e.preventDefault();
|
||||
};
|
||||
element.addEventListener('click', click);
|
||||
element.addEventListener('pointerdown', start, true);
|
||||
element.addEventListener('pointerdown', start, { capture: true });
|
||||
element.addEventListener('pointerup', clearLongPressTimer, { capture: true, passive: true });
|
||||
return {
|
||||
destroy: () => {
|
||||
|
||||
@@ -173,10 +173,12 @@
|
||||
});
|
||||
|
||||
const slideshowNavigationUnsubscribe = slideshowNavigation.subscribe((value) => {
|
||||
if (value === SlideshowNavigation.Shuffle) {
|
||||
slideshowHistory.reset();
|
||||
slideshowHistory.queue(toTimelineAsset(asset));
|
||||
if (value !== SlideshowNavigation.Shuffle) {
|
||||
return;
|
||||
}
|
||||
|
||||
slideshowHistory.reset();
|
||||
slideshowHistory.queue(toTimelineAsset(asset));
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -477,9 +479,7 @@
|
||||
|
||||
if (event.detail.direction === 'left') {
|
||||
navigateAsset('next');
|
||||
}
|
||||
|
||||
if (event.detail.direction === 'right') {
|
||||
} else if (event.detail.direction === 'right') {
|
||||
navigateAsset('previous');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -191,9 +191,9 @@
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
{#if (asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth) || asset.exifInfo?.fileSizeInByte}
|
||||
{#if (asset.exifInfo?.exifImageHeight && asset.exifInfo.exifImageWidth) || asset.exifInfo?.fileSizeInByte}
|
||||
<div class="flex gap-2 text-sm">
|
||||
{#if asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth}
|
||||
{#if asset.exifInfo?.exifImageHeight && asset.exifInfo.exifImageWidth}
|
||||
{#if getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}
|
||||
<p>
|
||||
{getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} MP
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
let formattedAge;
|
||||
if (ageInYears < 0) {
|
||||
return { formattedBirthDate: undefined, formattedAge: undefined, ...person };
|
||||
} else if (ageInMonths < 12) {
|
||||
}
|
||||
if (ageInMonths < 12) {
|
||||
formattedAge = $t('age_months', { values: { months: ageInMonths } });
|
||||
} else if (ageInMonths > 12 && ageInMonths < 24) {
|
||||
formattedAge = $t('age_year_months', { values: { months: ageInMonths - 12 } });
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
const handleSelectStart = (event: Event) => {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
requestAnimationFrame(() => {
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-global-this
|
||||
const selection = globalThis.getSelection();
|
||||
if (selection) {
|
||||
selection.selectAllChildren(target);
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
|
||||
// TODO move to action + command palette
|
||||
const onCopyShortcut = (event: KeyboardEvent) => {
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-global-this
|
||||
if (globalThis.getSelection()?.type === 'Range') {
|
||||
return;
|
||||
}
|
||||
@@ -139,7 +140,7 @@
|
||||
if (!url || !castManager.isCasting) {
|
||||
return;
|
||||
}
|
||||
const fullUrl = new URL(url, globalThis.location.href);
|
||||
const fullUrl = new URL(url, location.href);
|
||||
|
||||
try {
|
||||
await castManager.loadMedia(fullUrl.href);
|
||||
|
||||
@@ -61,10 +61,12 @@
|
||||
|
||||
const hideControlsAfterDelay = () => {
|
||||
timer = setTimeout(() => {
|
||||
if (!isOverControls) {
|
||||
showControls = false;
|
||||
setCursorStyle('none');
|
||||
if (isOverControls) {
|
||||
return;
|
||||
}
|
||||
|
||||
showControls = false;
|
||||
setCursorStyle('none');
|
||||
}, 2500);
|
||||
};
|
||||
|
||||
@@ -77,10 +79,12 @@
|
||||
});
|
||||
|
||||
unsubscribeStop = stopProgress.subscribe((value) => {
|
||||
if (value) {
|
||||
progressBar?.restart();
|
||||
stopControlsHideTimer();
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
progressBar?.restart();
|
||||
stopControlsHideTimer();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
const description = $derived(asset.exifInfo?.description?.trim() || '');
|
||||
|
||||
const dateTime = $derived(
|
||||
asset.exifInfo?.timeZone && asset.exifInfo?.dateTimeOriginal
|
||||
asset.exifInfo?.timeZone && asset.exifInfo.dateTimeOriginal
|
||||
? fromISODateTime(asset.exifInfo.dateTimeOriginal, asset.exifInfo.timeZone)
|
||||
: fromISODateTimeUTC(asset.localDateTime),
|
||||
);
|
||||
|
||||
@@ -152,13 +152,13 @@
|
||||
},
|
||||
useMediaCapabilities: false,
|
||||
xhrSetup: (xhr: XMLHttpRequest, url: string) => {
|
||||
const authenticatedUrl = new URL(url, globalThis.location.origin);
|
||||
const authenticatedUrl = new URL(url, location.origin);
|
||||
for (const [key, value] of Object.entries(authManager.params)) {
|
||||
if (value) {
|
||||
authenticatedUrl.searchParams.set(key, value as string);
|
||||
}
|
||||
}
|
||||
xhr.open('GET', authenticatedUrl.toString());
|
||||
xhr.open('GET', authenticatedUrl.href);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -308,8 +308,7 @@
|
||||
const onSwipe = (event: SwipeCustomEvent) => {
|
||||
if (event.detail.direction === 'left') {
|
||||
onNextAsset();
|
||||
}
|
||||
if (event.detail.direction === 'right') {
|
||||
} else if (event.detail.direction === 'right') {
|
||||
onPreviousAsset();
|
||||
}
|
||||
};
|
||||
@@ -389,10 +388,12 @@
|
||||
onended={onVideoEnded}
|
||||
onseeking={onSeeking}
|
||||
onplaying={(e: Event) => {
|
||||
if (!hasFocused) {
|
||||
(e.currentTarget as HTMLElement).focus();
|
||||
hasFocused = true;
|
||||
if (hasFocused) {
|
||||
return;
|
||||
}
|
||||
|
||||
(e.currentTarget as HTMLElement).focus();
|
||||
hasFocused = true;
|
||||
}}
|
||||
onclose={onClose}
|
||||
poster={getAssetMediaUrl({ id: asset.id, size: AssetMediaSize.Preview, cacheKey })}
|
||||
@@ -412,10 +413,12 @@
|
||||
onended={onVideoEnded}
|
||||
onseeking={onSeeking}
|
||||
onplaying={(e) => {
|
||||
if (!hasFocused) {
|
||||
e.currentTarget.focus();
|
||||
hasFocused = true;
|
||||
if (hasFocused) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.currentTarget.focus();
|
||||
hasFocused = true;
|
||||
}}
|
||||
onclose={onClose}
|
||||
poster={getAssetMediaUrl({ id: asset.id, size: AssetMediaSize.Preview, cacheKey })}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
if (!url || !castManager.isCasting) {
|
||||
return;
|
||||
}
|
||||
const fullUrl = new URL(url, globalThis.location.href);
|
||||
const fullUrl = new URL(url, location.href);
|
||||
|
||||
try {
|
||||
await castManager.loadMedia(fullUrl.href, force);
|
||||
@@ -63,7 +63,7 @@
|
||||
};
|
||||
|
||||
function handleSeek(event: Event) {
|
||||
const newTime = Number.parseFloat((event.target as HTMLInputElement).value);
|
||||
const newTime = Number((event.target as HTMLInputElement).value);
|
||||
castManager.seekTo(newTime);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -37,9 +37,8 @@
|
||||
if (isRotated) {
|
||||
let [width, height] = ratio.value.split(':');
|
||||
return `${height}:${width}`;
|
||||
} else {
|
||||
return ratio.value;
|
||||
}
|
||||
return ratio.value;
|
||||
}
|
||||
|
||||
function ratioSelected(ratio: AspectRatioOption): boolean {
|
||||
|
||||
@@ -49,6 +49,6 @@ class ImmichTimeRange extends MediaTimeRange {
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.customElements.get('immich-time-range')) {
|
||||
globalThis.customElements.define('immich-time-range', ImmichTimeRange);
|
||||
if (!customElements.get('immich-time-range')) {
|
||||
customElements.define('immich-time-range', ImmichTimeRange);
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
e.preventDefault();
|
||||
};
|
||||
element.addEventListener('click', click);
|
||||
element.addEventListener('pointerdown', start, true);
|
||||
element.addEventListener('pointerdown', start, { capture: true });
|
||||
element.addEventListener('pointerup', clearLongPressTimer, { capture: true, passive: true });
|
||||
return {
|
||||
destroy: () => {
|
||||
@@ -215,8 +215,7 @@
|
||||
onkeydown={(evt) => {
|
||||
if (evt.key === 'Enter') {
|
||||
callClickHandlers();
|
||||
}
|
||||
if (evt.key === 'x') {
|
||||
} else if (evt.key === 'x') {
|
||||
onSelect?.(asset);
|
||||
}
|
||||
if (document.activeElement === element && evt.key === 'Escape') {
|
||||
|
||||
@@ -85,10 +85,7 @@
|
||||
}}
|
||||
ontimeupdate={({ currentTarget }) => {
|
||||
const remaining = currentTarget.duration - currentTarget.currentTime;
|
||||
remainingSeconds = Math.min(
|
||||
Math.ceil(Number.isNaN(remaining) ? Number.POSITIVE_INFINITY : remaining),
|
||||
durationInSeconds,
|
||||
);
|
||||
remainingSeconds = Math.min(Math.ceil(Number.isNaN(remaining) ? Infinity : remaining), durationInSeconds);
|
||||
}}
|
||||
></video>
|
||||
{/if}
|
||||
|
||||
@@ -93,10 +93,10 @@
|
||||
};
|
||||
|
||||
const handleReset = (id: string) => {
|
||||
if (selectedPersonToReassign[id]) {
|
||||
if (Object.hasOwn(selectedPersonToReassign, id)) {
|
||||
delete selectedPersonToReassign[id];
|
||||
}
|
||||
if (selectedPersonToCreate[id]) {
|
||||
if (Object.hasOwn(selectedPersonToCreate, id)) {
|
||||
delete selectedPersonToCreate[id];
|
||||
}
|
||||
};
|
||||
@@ -115,7 +115,7 @@
|
||||
id: personId,
|
||||
faceDto: { id: personWithFace.id },
|
||||
});
|
||||
} else if (selectedPersonToCreate[personWithFace.id]) {
|
||||
} else if (Object.hasOwn(selectedPersonToCreate, personWithFace.id)) {
|
||||
const data = await createPerson({ personCreateDto: {} });
|
||||
peopleToCreate.push(data.id);
|
||||
await reassignFacesById({
|
||||
@@ -314,7 +314,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !selectedPersonToCreate[face.id]}
|
||||
{#if !Object.hasOwn(selectedPersonToCreate, face.id)}
|
||||
<p class="relative mt-1 truncate font-medium" title={personName}>
|
||||
{#if selectedPersonToReassign[face.id]?.id}
|
||||
{selectedPersonToReassign[face.id]?.name}
|
||||
@@ -349,7 +349,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="absolute inset-e-8 top-[-3px] size-5 rounded-full">
|
||||
{#if !selectedPersonToCreate[face.id] && !selectedPersonToReassign[face.id] && !face.person}
|
||||
{#if !Object.hasOwn(selectedPersonToCreate, face.id) && !Object.hasOwn(selectedPersonToReassign, face.id) && !face.person}
|
||||
<div
|
||||
class="absolute inset-s-1/2 top-1/2 flex translate-[-50%] transform place-content-center place-items-center rounded-full bg-[#d3d3d3] p-1 transition-all"
|
||||
>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
}
|
||||
|
||||
// Sort by date descending (newest first), but put unknown date at the top
|
||||
const sortedEntries = [...groups.entries()].sort((a, b) => {
|
||||
const sortedEntries = [...groups].sort((a, b) => {
|
||||
if (a[0] === unknownDateKey) {
|
||||
return -1;
|
||||
}
|
||||
@@ -115,7 +115,7 @@
|
||||
|
||||
<hr />
|
||||
|
||||
{#each [...groupedBackups.entries()] as [dateGroup, groupBackups] (dateGroup)}
|
||||
{#each [...groupedBackups] as [dateGroup, groupBackups] (dateGroup)}
|
||||
<Stack gap={2}>
|
||||
<div class="mt-5 mb-1">
|
||||
<div class="flex w-max place-items-center gap-2 rounded-xl bg-primary-50 px-4 py-2">
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
let length = 13;
|
||||
if (data) {
|
||||
const valueLength = data.value.toString().length;
|
||||
length = length - valueLength;
|
||||
length -= valueLength;
|
||||
}
|
||||
|
||||
return '0'.repeat(length);
|
||||
|
||||
@@ -35,10 +35,12 @@
|
||||
let assets = $derived(sharedLink.assets);
|
||||
|
||||
dragAndDropFilesStore.subscribe((value) => {
|
||||
if (value.isDragging && value.files.length > 0) {
|
||||
handlePromiseError(handleUploadAssets(value.files));
|
||||
dragAndDropFilesStore.set({ isDragging: false, files: [] });
|
||||
if (!(value.isDragging && value.files.length > 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handlePromiseError(handleUploadAssets(value.files));
|
||||
dragAndDropFilesStore.set({ isDragging: false, files: [] });
|
||||
});
|
||||
|
||||
const downloadAssets = async () => {
|
||||
|
||||
@@ -345,10 +345,12 @@
|
||||
{
|
||||
shortcut: { key: 'Escape' },
|
||||
onShortcut: (event) => {
|
||||
if (isOpen) {
|
||||
event.stopPropagation();
|
||||
closeDropdown();
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
closeDropdown();
|
||||
},
|
||||
},
|
||||
]}
|
||||
@@ -399,7 +401,7 @@
|
||||
aria-selected={selectedIndex === 0}
|
||||
aria-disabled={true}
|
||||
class="w-full cursor-default px-4 py-2 text-start hover:bg-gray-200 aria-selected:bg-gray-200 dark:hover:bg-gray-700 aria-selected:dark:bg-gray-700"
|
||||
id={`${listboxId}-${0}`}
|
||||
id={`${listboxId}-0`}
|
||||
onclick={closeDropdown}
|
||||
>
|
||||
{allowCreate ? searchQuery : $t('no_results')}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
};
|
||||
};
|
||||
|
||||
const parsePixels = (style: string) => Number.parseInt(style, 10) || 0;
|
||||
const parsePixels = (style: string) => Math.trunc(Number(style)) || 0;
|
||||
|
||||
const getItemCount = (container: HTMLElement, containerWidth: number) => {
|
||||
if (!container.firstElementChild) {
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
let isActive = $derived($selectedIdStore === id);
|
||||
|
||||
const handleClick = () => {
|
||||
// eslint-disable-next-line unicorn/no-optional-chaining-on-undeclared-variable
|
||||
$optionClickCallbackStore?.();
|
||||
onClick();
|
||||
};
|
||||
|
||||
@@ -57,11 +57,13 @@
|
||||
onClose?.();
|
||||
};
|
||||
$effect(() => {
|
||||
if (isOpen && menuContainer) {
|
||||
triggerElement = document.activeElement as HTMLElement;
|
||||
menuContainer.focus();
|
||||
$optionClickCallbackStore = closeContextMenu;
|
||||
if (!(isOpen && menuContainer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerElement = document.activeElement as HTMLElement;
|
||||
menuContainer.focus();
|
||||
$optionClickCallbackStore = closeContextMenu;
|
||||
});
|
||||
|
||||
const oncontextmenu = async (event: MouseEvent) => {
|
||||
|
||||
@@ -109,12 +109,14 @@
|
||||
|
||||
let lastEndReachedHeight = 0;
|
||||
$effect(() => {
|
||||
if (geometry.containerHeight - slidingWindow.bottom <= viewport.height) {
|
||||
const contentHeight = geometry.containerHeight;
|
||||
if (lastEndReachedHeight !== contentHeight) {
|
||||
debouncedOnEndReached();
|
||||
lastEndReachedHeight = contentHeight;
|
||||
}
|
||||
if (geometry.containerHeight - slidingWindow.bottom > viewport.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentHeight = geometry.containerHeight;
|
||||
if (lastEndReachedHeight !== contentHeight) {
|
||||
debouncedOnEndReached();
|
||||
lastEndReachedHeight = contentHeight;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -109,14 +109,16 @@
|
||||
);
|
||||
|
||||
export function addClipMapMarker(lng: number, lat: number) {
|
||||
if (map) {
|
||||
if (marker) {
|
||||
marker.remove();
|
||||
}
|
||||
|
||||
center = { lng, lat };
|
||||
marker = new Marker().setLngLat([lng, lat]).addTo(map);
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (marker) {
|
||||
marker.remove();
|
||||
}
|
||||
|
||||
center = { lng, lat };
|
||||
marker = new Marker().setLngLat([lng, lat]).addTo(map);
|
||||
}
|
||||
|
||||
function handleAssetClick(assetId: string, map: Map | null) {
|
||||
@@ -159,17 +161,19 @@
|
||||
}
|
||||
|
||||
function handleMapClick(event: MapMouseEvent) {
|
||||
if (clickable) {
|
||||
const { lng, lat } = event.lngLat;
|
||||
onClickPoint({ lng, lat });
|
||||
if (!clickable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (marker) {
|
||||
marker.remove();
|
||||
}
|
||||
const { lng, lat } = event.lngLat;
|
||||
onClickPoint({ lng, lat });
|
||||
|
||||
if (map) {
|
||||
marker = new Marker().setLngLat([lng, lat]).addTo(map);
|
||||
}
|
||||
if (marker) {
|
||||
marker.remove();
|
||||
}
|
||||
|
||||
if (map) {
|
||||
marker = new Marker().setLngLat([lng, lat]).addTo(map);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,13 +258,16 @@
|
||||
};
|
||||
|
||||
afterNavigate(() => {
|
||||
if (map) {
|
||||
map.resize();
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (globalThis.location.hash) {
|
||||
const hashChangeEvent = new HashChangeEvent('hashchange');
|
||||
globalThis.dispatchEvent(hashChangeEvent);
|
||||
}
|
||||
map.resize();
|
||||
|
||||
if (location.hash) {
|
||||
const hashChangeEvent = new HashChangeEvent('hashchange');
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-global-this
|
||||
globalThis.dispatchEvent(hashChangeEvent);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -156,10 +156,12 @@
|
||||
};
|
||||
|
||||
const onEnter = (event: KeyboardEvent) => {
|
||||
if (selectedId) {
|
||||
event.preventDefault();
|
||||
searchHistoryBox?.selectActiveOption();
|
||||
if (!selectedId) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
searchHistoryBox?.selectActiveOption();
|
||||
};
|
||||
|
||||
const onInput = () => {
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
export function moveSelection(increment: 1 | -1) {
|
||||
if (!isSearchSuggestions) {
|
||||
return;
|
||||
} else if (selectedIndex === undefined) {
|
||||
}
|
||||
if (selectedIndex === undefined) {
|
||||
selectedIndex = increment === 1 ? 0 : suggestionCount - 1;
|
||||
} else if (selectedIndex + increment < 0 || selectedIndex + increment >= suggestionCount) {
|
||||
clearSelection();
|
||||
|
||||
@@ -18,11 +18,13 @@
|
||||
const defaultLangOption = { label: defaultLang.name, value: defaultLang.code };
|
||||
|
||||
const handleLanguageChange = async (newLang: string | undefined) => {
|
||||
if (newLang) {
|
||||
$lang = newLang;
|
||||
await i18nLocale.set(convertBCP47(newLang));
|
||||
await invalidateAll();
|
||||
if (!newLang) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lang = newLang;
|
||||
await i18nLocale.set(convertBCP47(newLang));
|
||||
await invalidateAll();
|
||||
};
|
||||
|
||||
let closestLanguage = $derived(getClosestAvailableLocale([$lang], langCodes));
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
const refreshAlbums = async () => {
|
||||
try {
|
||||
const allAlbums = await getAllAlbums({});
|
||||
albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3);
|
||||
albums = allAlbums.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()).slice(0, 3);
|
||||
userInteraction.recentAlbums = albums;
|
||||
} catch (error) {
|
||||
handleError(error, $t('failed_to_load_assets'));
|
||||
|
||||
@@ -99,13 +99,15 @@
|
||||
) => {
|
||||
if (scrubberMonth === 'lead-in') {
|
||||
return relativeTopOffset * scrubberMonthPercent;
|
||||
} else if (scrubberMonth === 'lead-out') {
|
||||
}
|
||||
if (scrubberMonth === 'lead-out') {
|
||||
let offset = relativeTopOffset;
|
||||
for (const segment of segments) {
|
||||
offset += segment.height;
|
||||
}
|
||||
return offset + relativeBottomOffset * scrubberMonthPercent;
|
||||
} else if (scrubberMonth) {
|
||||
}
|
||||
if (scrubberMonth) {
|
||||
let offset = relativeTopOffset;
|
||||
let match = false;
|
||||
for (const segment of segments) {
|
||||
@@ -120,9 +122,8 @@
|
||||
offset += scrubberMonthPercent * relativeBottomOffset;
|
||||
}
|
||||
return offset;
|
||||
} else {
|
||||
return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM));
|
||||
}
|
||||
return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM));
|
||||
};
|
||||
const scrollY = $derived(
|
||||
toScrollFromTimelineMonthPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
|
||||
@@ -229,14 +230,14 @@
|
||||
if (scrollY !== undefined) {
|
||||
if (scrollY < relativeTopOffset) {
|
||||
return segments.at(0)?.dateFormatted;
|
||||
} else {
|
||||
let offset = relativeTopOffset;
|
||||
for (const segment of segments) {
|
||||
offset += segment.height;
|
||||
}
|
||||
if (scrollY > offset) {
|
||||
return segments.at(-1)?.dateFormatted;
|
||||
}
|
||||
}
|
||||
|
||||
let offset = relativeTopOffset;
|
||||
for (const segment of segments) {
|
||||
offset += segment.height;
|
||||
}
|
||||
if (scrollY > offset) {
|
||||
return segments.at(-1)?.dateFormatted;
|
||||
}
|
||||
}
|
||||
return scrollSegment?.dateFormatted || '';
|
||||
@@ -338,7 +339,7 @@
|
||||
overallScrollPercent: toTimelineY(hoverY),
|
||||
scrubberMonthScrollPercent: timelineMonthPercentY,
|
||||
};
|
||||
if (wasDragging === false && isDragging) {
|
||||
if (!wasDragging && isDragging) {
|
||||
void startScrub?.(scrubData);
|
||||
void onScrub?.(scrubData);
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
export const scrollAfterNavigate = async () => {
|
||||
if (timelineManager.viewportHeight === 0 || timelineManager.viewportWidth === 0) {
|
||||
// this can happen if you do the following navigation order
|
||||
// /photos?at=<id>, /photos/<id>, http://example.com, browser back, browser back
|
||||
// /photos?at=<id>, /photos/<id>, https://example.com, browser back, browser back
|
||||
const rect = scrollableElement?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
timelineManager.viewportHeight = rect.height;
|
||||
@@ -209,10 +209,7 @@
|
||||
}
|
||||
}
|
||||
const scrollTarget = assetViewerManager.gridScrollTarget?.at;
|
||||
let scrolled = false;
|
||||
if (scrollTarget) {
|
||||
scrolled = await scrollAndLoadAsset(scrollTarget);
|
||||
}
|
||||
const scrolled = scrollTarget ? await scrollAndLoadAsset(scrollTarget) : false;
|
||||
if (!scrolled) {
|
||||
// if the asset is not found, scroll to the top
|
||||
timelineManager.scrollTo(0);
|
||||
@@ -503,10 +500,12 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (assetViewerManager.asset && assetViewerManager.isViewing) {
|
||||
const { localDateTime } = getTimes(assetViewerManager.asset.fileCreatedAt, DateTime.local().offset / 60);
|
||||
void timelineManager.loadTimelineMonth({ year: localDateTime.year, month: localDateTime.month });
|
||||
if (!(assetViewerManager.asset && assetViewerManager.isViewing)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { localDateTime } = getTimes(assetViewerManager.asset.fileCreatedAt, DateTime.local().offset / 60);
|
||||
void timelineManager.loadTimelineMonth({ year: localDateTime.year, month: localDateTime.month });
|
||||
});
|
||||
|
||||
const assetSelectHandler = (
|
||||
@@ -582,10 +581,7 @@
|
||||
bind:scrubberWidth
|
||||
onScrubKeyDown={(evt) => {
|
||||
evt.preventDefault();
|
||||
let amount = 50;
|
||||
if (keyboardManager.shift) {
|
||||
amount = 500;
|
||||
}
|
||||
let amount = keyboardManager.shift ? 500 : 50;
|
||||
if (evt.key === 'ArrowUp') {
|
||||
amount = -amount;
|
||||
if (keyboardManager.shift) {
|
||||
|
||||
@@ -41,10 +41,12 @@
|
||||
|
||||
for (const option of Object.values(element.options)) {
|
||||
for (const pluralElement of option.value) {
|
||||
if (pluralElement.type === TYPE.tag) {
|
||||
const tag = pluralElement.value;
|
||||
replacements[tag] = (...parts) => `<${tag}>${parts}</${tag}>`;
|
||||
if (pluralElement.type !== TYPE.tag) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tag = pluralElement.value;
|
||||
replacements[tag] = (...parts) => `<${tag}>${parts}</${tag}>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,10 +134,12 @@ class AssetViewerManager extends BaseEventManager<Events> {
|
||||
}
|
||||
|
||||
cancelZoomAnimation() {
|
||||
if (this.#animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.#animationFrameId);
|
||||
this.#animationFrameId = null;
|
||||
if (this.#animationFrameId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelAnimationFrame(this.#animationFrameId);
|
||||
this.#animationFrameId = null;
|
||||
}
|
||||
|
||||
animatedZoom(targetZoom: number, duration = 300) {
|
||||
|
||||
@@ -109,7 +109,7 @@ class AuthManager {
|
||||
|
||||
await goto(redirectUri);
|
||||
} else {
|
||||
globalThis.location.href = redirectUri;
|
||||
location.assign(redirectUri);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class DownloadManager {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.assets[key]) {
|
||||
if (!Object.hasOwn(this.assets, key)) {
|
||||
this.assets[key] = { progress: 0, total: 0, percentage: 0, abort: null };
|
||||
}
|
||||
|
||||
|
||||
@@ -193,6 +193,7 @@ class TransformManager implements EditToolManager {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-global-this
|
||||
globalThis.addEventListener('mousemove', (e: MouseEvent) => transformManager.handleMouseMove(e), { passive: true });
|
||||
|
||||
const transformEdits = edits.filter((e) => e.action === 'rotate' || e.action === 'mirror');
|
||||
@@ -210,6 +211,7 @@ class TransformManager implements EditToolManager {
|
||||
}
|
||||
|
||||
onDeactivate() {
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-global-this
|
||||
globalThis.removeEventListener('mousemove', transformManager.handleMouseMove);
|
||||
|
||||
this.reset();
|
||||
@@ -553,6 +555,7 @@ class TransformManager implements EditToolManager {
|
||||
}
|
||||
|
||||
document.body.style.userSelect = 'none';
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-global-this
|
||||
globalThis.addEventListener('mouseup', () => this.handleMouseUp(), { passive: true });
|
||||
}
|
||||
|
||||
@@ -571,6 +574,7 @@ class TransformManager implements EditToolManager {
|
||||
}
|
||||
|
||||
handleMouseUp() {
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-global-this
|
||||
globalThis.removeEventListener('mouseup', this.handleMouseUp);
|
||||
document.body.style.userSelect = '';
|
||||
|
||||
|
||||
@@ -13,10 +13,11 @@ class LanguageManager {
|
||||
rtl = $state(false);
|
||||
|
||||
init() {
|
||||
if (!this.initialized) {
|
||||
this.initialized = true;
|
||||
lang.subscribe((lang) => this.setLanguage(lang));
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
this.initialized = true;
|
||||
lang.subscribe((lang) => this.setLanguage(lang));
|
||||
}
|
||||
|
||||
setLanguage(code: string) {
|
||||
|
||||
@@ -89,4 +89,5 @@ class MediaCapabilitiesManager {
|
||||
}
|
||||
|
||||
export const mediaCapabilitiesManager = new MediaCapabilitiesManager();
|
||||
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
||||
mediaCapabilitiesManager.init();
|
||||
|
||||
@@ -19,10 +19,10 @@ export class GroupInsertionCache {
|
||||
}
|
||||
|
||||
setTimelineDay(timelineDay: TimelineDay, { year, month, day }: TimelineDate) {
|
||||
if (!this.#lookupCache[year]) {
|
||||
if (!Object.hasOwn(this.#lookupCache, year)) {
|
||||
this.#lookupCache[year] = {};
|
||||
}
|
||||
if (!this.#lookupCache[year][month]) {
|
||||
if (!Object.hasOwn(this.#lookupCache[year], month)) {
|
||||
this.#lookupCache[year][month] = {};
|
||||
}
|
||||
this.#lookupCache[year][month][day] = timelineDay;
|
||||
|
||||
@@ -41,8 +41,6 @@ export function layoutTimelineMonth(timelineManager: TimelineManager, month: Tim
|
||||
timelineDay.col = timelineDayCol++;
|
||||
timelineDay.start = cumulativeWidth;
|
||||
timelineDay.top = cumulativeHeight;
|
||||
|
||||
cumulativeWidth += timelineDay.width + timelineManager.gap;
|
||||
} else {
|
||||
// Move to next row
|
||||
cumulativeHeight += currentRowHeight;
|
||||
@@ -57,8 +55,8 @@ export function layoutTimelineMonth(timelineManager: TimelineManager, month: Tim
|
||||
timelineDay.top = cumulativeHeight;
|
||||
|
||||
timelineDayCol++;
|
||||
cumulativeWidth += timelineDay.width + timelineManager.gap;
|
||||
}
|
||||
cumulativeWidth += timelineDay.width + timelineManager.gap;
|
||||
currentRowHeight = timelineDay.height + timelineManager.headerHeight;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export function updateObject(target: any, source: any): boolean {
|
||||
}
|
||||
const isDate = target[key] instanceof Date;
|
||||
if (typeof target[key] === 'object' && !isDate) {
|
||||
updated = updated || updateObject(target[key], source[key]);
|
||||
updated ||= updateObject(target[key], source[key]);
|
||||
} else {
|
||||
if (target[key] !== source[key]) {
|
||||
target[key] = source[key];
|
||||
|
||||
@@ -357,10 +357,7 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
}
|
||||
|
||||
async loadTimelineMonth(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }): Promise<void> {
|
||||
let cancelable = true;
|
||||
if (options) {
|
||||
cancelable = options.cancelable;
|
||||
}
|
||||
const cancelable = options?.cancelable ?? true;
|
||||
const timelineMonth = getTimelineMonthByDate(this, yearMonth);
|
||||
if (!timelineMonth) {
|
||||
return;
|
||||
@@ -517,10 +514,7 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const idsToUpdate = new Set(cache.keys());
|
||||
const result = this.#runAssetCallback(idsToUpdate, (asset) => void updateObject(asset, cache.get(asset.id)));
|
||||
const notUpdated: TimelineAsset[] = [];
|
||||
for (const assetId of result.notUpdated) {
|
||||
notUpdated.push(cache.get(assetId)!);
|
||||
}
|
||||
const notUpdated: TimelineAsset[] = Array.from(result.notUpdated, (assetId) => cache.get(assetId)!);
|
||||
return notUpdated;
|
||||
}
|
||||
|
||||
|
||||
@@ -145,21 +145,23 @@ export class TimelineMonth {
|
||||
const combinedMoveAssets: MoveAsset[][] = [];
|
||||
let index = timelineDays.length;
|
||||
while (index--) {
|
||||
if (idsToProcess.size > 0) {
|
||||
const group = timelineDays[index];
|
||||
const { moveAssets, processedIds, changedGeometry } = group.runAssetCallback(ids, callback);
|
||||
if (moveAssets.length > 0) {
|
||||
combinedMoveAssets.push(moveAssets);
|
||||
}
|
||||
idsToProcess = setDifference(idsToProcess, processedIds);
|
||||
for (const id of processedIds) {
|
||||
idsProcessed.add(id);
|
||||
}
|
||||
combinedChangedGeometry = combinedChangedGeometry || changedGeometry;
|
||||
if (group.viewerAssets.length === 0) {
|
||||
timelineDays.splice(index, 1);
|
||||
combinedChangedGeometry = true;
|
||||
}
|
||||
if (idsToProcess.size === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const group = timelineDays[index];
|
||||
const { moveAssets, processedIds, changedGeometry } = group.runAssetCallback(ids, callback);
|
||||
if (moveAssets.length > 0) {
|
||||
combinedMoveAssets.push(moveAssets);
|
||||
}
|
||||
idsToProcess = setDifference(idsToProcess, processedIds);
|
||||
for (const id of processedIds) {
|
||||
idsProcessed.add(id);
|
||||
}
|
||||
combinedChangedGeometry ||= changedGeometry;
|
||||
if (group.viewerAssets.length === 0) {
|
||||
timelineDays.splice(index, 1);
|
||||
combinedChangedGeometry = true;
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -195,7 +197,7 @@ export class TimelineMonth {
|
||||
ownerId: bucketAssets.ownerId[i],
|
||||
projectionType: bucketAssets.projectionType[i],
|
||||
ratio: bucketAssets.ratio[i],
|
||||
stack: bucketAssets.stack?.[i]
|
||||
stack: bucketAssets.stack?.at(i)
|
||||
? {
|
||||
id: bucketAssets.stack[i]![0],
|
||||
primaryAssetId: bucketAssets.id[i],
|
||||
@@ -206,7 +208,7 @@ export class TimelineMonth {
|
||||
people: null, // People are not included in the bucket assets
|
||||
};
|
||||
|
||||
if (bucketAssets.latitude?.[i] && bucketAssets.longitude?.[i]) {
|
||||
if (bucketAssets.latitude?.at(i) && bucketAssets.longitude?.at(i)) {
|
||||
timelineAsset.latitude = bucketAssets.latitude?.[i];
|
||||
timelineAsset.longitude = bucketAssets.longitude?.[i];
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
break;
|
||||
}
|
||||
case 'Control': {
|
||||
// eslint-disable-next-line unicorn/no-late-event-control
|
||||
e.preventDefault();
|
||||
handleMultiSelect();
|
||||
break;
|
||||
|
||||
@@ -85,8 +85,8 @@
|
||||
// Try to parse coordinate pair from search input in the format `LATITUDE, LONGITUDE` as floats
|
||||
const coordinateParts = searchWord.split(',').map((part) => part.trim());
|
||||
if (coordinateParts.length === 2) {
|
||||
const coordinateLat = Number.parseFloat(coordinateParts[0]);
|
||||
const coordinateLng = Number.parseFloat(coordinateParts[1]);
|
||||
const coordinateLat = Number(coordinateParts[0]);
|
||||
const coordinateLng = Number(coordinateParts[1]);
|
||||
|
||||
if (
|
||||
!Number.isNaN(coordinateLat) &&
|
||||
@@ -106,18 +106,22 @@
|
||||
searchPlaces({ name: searchWord })
|
||||
.then((searchResult) => {
|
||||
// skip result when a newer search is happening
|
||||
if (latestSearchTimeout === searchTimeout) {
|
||||
places = searchResult;
|
||||
showLoadingSpinner = false;
|
||||
if (latestSearchTimeout !== searchTimeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
places = searchResult;
|
||||
showLoadingSpinner = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
// skip error when a newer search is happening
|
||||
if (latestSearchTimeout === searchTimeout) {
|
||||
places = [];
|
||||
handleError(error, $t('errors.cant_search_places'));
|
||||
showLoadingSpinner = false;
|
||||
if (latestSearchTimeout !== searchTimeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
places = [];
|
||||
handleError(error, $t('errors.cant_search_places'));
|
||||
showLoadingSpinner = false;
|
||||
});
|
||||
}, timeDebounceOnSearch);
|
||||
latestSearchTimeout = searchTimeout;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
const changePersonToMerge = (newPerson: PersonResponseDto) => {
|
||||
const index = potentialMergePeople.indexOf(newPerson);
|
||||
// eslint-disable-next-line unicorn/no-unreadable-array-destructuring
|
||||
[potentialMergePeople[index], personToBeMergedInto] = [personToBeMergedInto, potentialMergePeople[index]];
|
||||
choosePersonToMerge = false;
|
||||
};
|
||||
|
||||
@@ -45,10 +45,8 @@
|
||||
}
|
||||
|
||||
const asFilter = (searchQuery: SmartSearchDto | MetadataSearchDto): SearchFilter => {
|
||||
let query = '';
|
||||
if ('query' in searchQuery && searchQuery.query) {
|
||||
query = searchQuery.query;
|
||||
}
|
||||
let query = 'query' in searchQuery && searchQuery.query ? searchQuery.query : '';
|
||||
|
||||
if ('originalFileName' in searchQuery && searchQuery.originalFileName) {
|
||||
query = searchQuery.originalFileName;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<ServerAboutItem id="build" title={$t('build')} version={info.build} versionHref={info.buildUrl} />
|
||||
{/if}
|
||||
|
||||
{#if info.buildImage && info.buildImage}
|
||||
{#if info.buildImage && info.buildImageUrl}
|
||||
<ServerAboutItem
|
||||
id="build-image"
|
||||
title={$t('build_image')}
|
||||
|
||||
@@ -37,7 +37,7 @@ export type ZoneOption = {
|
||||
valid: boolean;
|
||||
};
|
||||
|
||||
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const userTimeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const knownTimezones = Intl.supportedValuesOf('timeZone');
|
||||
|
||||
export function getTimezones(selectedDate: string) {
|
||||
|
||||
@@ -65,6 +65,7 @@ describe('Route', () => {
|
||||
describe(Route.continue.name, () => {
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error - override location for testing
|
||||
// eslint-disable-next-line unicorn/no-global-object-property-assignment
|
||||
globalThis.location = new URL('https://my.immich.server');
|
||||
vi.spyOn(document, 'baseURI', 'get').mockReturnValue('https://my.immich.server/');
|
||||
});
|
||||
|
||||
@@ -31,11 +31,7 @@ const asQueryString = (
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skipEmptyStrings && value === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return !(skipEmptyStrings && value === '');
|
||||
})
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export const handleDeleteDatabaseBackup = async (...filenames: string[]) => {
|
||||
};
|
||||
|
||||
export const handleDownloadDatabaseBackup = (filename: string) => {
|
||||
location.href = getBaseUrl() + '/admin/database-backups/' + filename;
|
||||
location.assign(getBaseUrl() + '/admin/database-backups/' + filename);
|
||||
};
|
||||
|
||||
export const handleUploadDatabaseBackup = async () => {
|
||||
|
||||
@@ -61,7 +61,7 @@ export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLin
|
||||
|
||||
export const asUrl = (sharedLink: SharedLinkResponseDto) => {
|
||||
const path = Route.viewSharedLink(sharedLink);
|
||||
return new URL(path, serverConfigManager.value.externalDomain || globalThis.location.origin).href;
|
||||
return new URL(path, serverConfigManager.value.externalDomain || location.origin).href;
|
||||
};
|
||||
|
||||
export const handleCreateSharedLink = async (dto: SharedLinkCreateDto) => {
|
||||
|
||||
@@ -64,7 +64,7 @@ export const handleSystemConfigSave = async (update: Partial<SystemConfigDto>) =
|
||||
};
|
||||
|
||||
export const handleUploadConfig = () => {
|
||||
const input = globalThis.document.createElement('input');
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
input.setAttribute('accept', '.json');
|
||||
input.setAttribute('style', 'display: none');
|
||||
@@ -83,6 +83,6 @@ export const handleUploadConfig = () => {
|
||||
.catch((error) => console.error('Error handling JSON config upload', error))
|
||||
.finally(() => input.remove());
|
||||
});
|
||||
globalThis.document.body.append(input);
|
||||
document.body.append(input);
|
||||
input.click();
|
||||
};
|
||||
|
||||
@@ -159,11 +159,11 @@ export const handleNavigateUserAdmin = async (user: UserAdminResponseDto) => {
|
||||
const generatePassword = (length: number = 16) => {
|
||||
let generatedPassword = '';
|
||||
|
||||
const characterSet = '0123456789' + 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + ',.-{}+!#$%/()=?';
|
||||
const characterSet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,.-{}+!#$%/()=?';
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
let randomNumber = crypto.getRandomValues(new Uint32Array(1))[0];
|
||||
randomNumber = randomNumber / 2 ** 32;
|
||||
randomNumber /= 2 ** 32;
|
||||
randomNumber = Math.floor(randomNumber * characterSet.length);
|
||||
|
||||
generatedPassword += characterSet[randomNumber];
|
||||
|
||||
@@ -3,8 +3,6 @@ import {
|
||||
deleteWorkflow,
|
||||
updateWorkflow,
|
||||
WorkflowTrigger,
|
||||
type AlbumResponseDto,
|
||||
type PersonResponseDto,
|
||||
type WorkflowCreateDto,
|
||||
type WorkflowResponseDto,
|
||||
type WorkflowUpdateDto,
|
||||
@@ -32,9 +30,6 @@ import { copyToClipboard, downloadJson } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
|
||||
export type PickerSubType = 'album-picker' | 'people-picker';
|
||||
export type PickerMetadata = AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[];
|
||||
|
||||
export const getWorkflowsActions = ($t: MessageFormatter) => {
|
||||
const Create: ActionItem = {
|
||||
title: $t('create_workflow'),
|
||||
|
||||
@@ -8,8 +8,11 @@ class KeyboardManager {
|
||||
if (globalThis.window === undefined) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-global-this
|
||||
globalThis.addEventListener('keydown', this.#update);
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-global-this
|
||||
globalThis.addEventListener('keyup', this.#update);
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-global-this
|
||||
globalThis.addEventListener('blur', this.#clear);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,10 +58,12 @@ function createSlideshowStore() {
|
||||
set: (value: boolean) => {
|
||||
// Trigger an action whenever the restartProgress is set to true. Automatically
|
||||
// reset the restart state after that
|
||||
if (value) {
|
||||
restartState.set(true);
|
||||
restartState.set(false);
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
restartState.set(true);
|
||||
restartState.set(false);
|
||||
},
|
||||
},
|
||||
stopProgress: {
|
||||
@@ -69,10 +71,12 @@ function createSlideshowStore() {
|
||||
set: (value: boolean) => {
|
||||
// Trigger an action whenever the stopProgress is set to true. Automatically
|
||||
// reset the stop state after that
|
||||
if (value) {
|
||||
stopState.set(true);
|
||||
stopState.set(false);
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopState.set(true);
|
||||
stopState.set(false);
|
||||
},
|
||||
},
|
||||
slideshowNavigation,
|
||||
|
||||
@@ -23,7 +23,7 @@ function createUploadStore() {
|
||||
|
||||
const addItem = (newAsset: UploadAsset) => {
|
||||
uploadAssets.update(($assets) => {
|
||||
const duplicate = $assets.find((asset) => asset.id === newAsset.id);
|
||||
const duplicate = $assets.some((asset) => asset.id === newAsset.id);
|
||||
if (duplicate) {
|
||||
return $assets.map((asset) => (asset.id === newAsset.id ? newAsset : asset));
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ const reset = () => {
|
||||
Object.assign(userInteraction, defaultUserInteraction);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
||||
eventManager.on({
|
||||
AlbumCreate: () => resetRecentAlbums(),
|
||||
AlbumUpdate: () => resetRecentAlbums(),
|
||||
|
||||
@@ -60,6 +60,7 @@ export const websocketStore = {
|
||||
|
||||
export const websocketEvents = createEventEmitter(websocket);
|
||||
|
||||
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
||||
websocket
|
||||
.on('connect', () => {
|
||||
eventManager.emit('WebsocketConnect');
|
||||
@@ -113,16 +114,18 @@ export const waitForWebsocketEvent = <T extends keyof Events>(
|
||||
return new Promise((resolve, reject) => {
|
||||
// @ts-expect-error: The typings are weird on this?
|
||||
const cleanup = websocketEvents.on(event, (...args: Parameters<Events[T]>) => {
|
||||
if (!predicate || predicate(...args)) {
|
||||
cleanup();
|
||||
clearTimeout(timer);
|
||||
resolve(args);
|
||||
if (predicate && !predicate(...args)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup();
|
||||
clearTimeout(timer);
|
||||
resolve(args);
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error(`Timeout waiting for event: ${String(event)}`));
|
||||
reject(new Error(`Timeout waiting for event: ${event}`));
|
||||
}, timeout);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -109,11 +109,10 @@ export const uploadRequest = async <T>(options: UploadRequestOptions): Promise<{
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
unsubscribe();
|
||||
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) {
|
||||
unsubscribe();
|
||||
resolve({ data: xhr.response as T, status: xhr.status });
|
||||
} else {
|
||||
unsubscribe();
|
||||
reject(new ApiError(xhr.statusText, xhr.status, xhr.response));
|
||||
}
|
||||
});
|
||||
@@ -326,9 +325,9 @@ export const oauth = {
|
||||
authorize: async (location: Location) => {
|
||||
const $t = get(t);
|
||||
try {
|
||||
const redirectUri = location.href.split('?')[0];
|
||||
const redirectUri = location.href.split('?', 1)[0];
|
||||
const { url } = await startOAuth({ oAuthConfigDto: { redirectUri } });
|
||||
globalThis.location.href = url;
|
||||
globalThis.location.assign(url);
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_login_with_oauth'));
|
||||
@@ -430,7 +429,8 @@ export const isEnabled = ({ $if }: IfLike) => $if?.() ?? true;
|
||||
export const transformToTitleCase = (text: string) => {
|
||||
if (text.length === 0) {
|
||||
return text;
|
||||
} else if (text.length === 1) {
|
||||
}
|
||||
if (text.length === 1) {
|
||||
return text.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
|
||||
@@ -68,19 +68,21 @@ const undoDeleteAssets = async (onUndoDelete: OnUndoDelete, assets: TimelineAsse
|
||||
* @param {StackResponse} stackResponse - The stack response containing the stack and assets to delete.
|
||||
*/
|
||||
export function updateStackedAssetInTimeline(timelineManager: TimelineManager, { stack, toDeleteIds }: StackResponse) {
|
||||
if (stack != undefined) {
|
||||
timelineManager.update(
|
||||
[stack.primaryAssetId],
|
||||
(asset) =>
|
||||
(asset.stack = {
|
||||
id: stack.id,
|
||||
primaryAssetId: stack.primaryAssetId,
|
||||
assetCount: stack.assets.length,
|
||||
}),
|
||||
);
|
||||
|
||||
timelineManager.removeAssets(toDeleteIds);
|
||||
if (stack == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
timelineManager.update(
|
||||
[stack.primaryAssetId],
|
||||
(asset) =>
|
||||
(asset.stack = {
|
||||
id: stack.id,
|
||||
primaryAssetId: stack.primaryAssetId,
|
||||
assetCount: stack.assets.length,
|
||||
}),
|
||||
);
|
||||
|
||||
timelineManager.removeAssets(toDeleteIds);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -86,6 +86,7 @@ export class AdaptiveImageLoader {
|
||||
|
||||
const config = this.qualityConfigs[quality];
|
||||
|
||||
// eslint-disable-next-line unicorn/no-computed-property-existence-check
|
||||
if (!this.status.urls[quality]) {
|
||||
return;
|
||||
}
|
||||
@@ -129,6 +130,7 @@ export class AdaptiveImageLoader {
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/no-computed-property-existence-check
|
||||
if (this.status.urls[quality]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -92,13 +92,13 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
|
||||
for (let index = 0; index < downloadInfo.archives.length; index++) {
|
||||
const archive = downloadInfo.archives[index];
|
||||
const suffix = downloadInfo.archives.length > 1 ? `+${index + 1}` : '';
|
||||
const archiveName = fileName.replace('.zip', `${suffix}-${DateTime.now().toFormat('yyyyLLdd_HHmmss')}.zip`);
|
||||
const archiveName = fileName.replace('.zip', () => `${suffix}-${DateTime.now().toFormat('yyyyLLdd_HHmmss')}.zip`);
|
||||
const queryParams = asQueryString(authManager.params);
|
||||
|
||||
let downloadKey = `${archiveName} `;
|
||||
if (downloadInfo.archives.length > 1) {
|
||||
downloadKey = `${archiveName} (${index + 1}/${downloadInfo.archives.length})`;
|
||||
}
|
||||
const downloadKey =
|
||||
downloadInfo.archives.length > 1
|
||||
? `${archiveName} (${index + 1}/${downloadInfo.archives.length})`
|
||||
: `${archiveName} `;
|
||||
|
||||
const abort = new AbortController();
|
||||
downloadManager.add(downloadKey, archive.size, abort);
|
||||
@@ -131,7 +131,7 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
|
||||
*/
|
||||
export function getFilenameExtension(filename: string): string {
|
||||
const lastIndex = Math.max(0, filename.lastIndexOf('.'));
|
||||
const startIndex = (lastIndex || Number.POSITIVE_INFINITY) + 1;
|
||||
const startIndex = (lastIndex || Infinity) + 1;
|
||||
return filename.slice(startIndex).toLowerCase();
|
||||
}
|
||||
|
||||
@@ -144,11 +144,11 @@ export function getAssetFilename(asset: AssetResponseDto): string {
|
||||
}
|
||||
|
||||
function isRotated90CW(orientation: number) {
|
||||
return orientation === 5 || orientation === 6 || orientation === 90;
|
||||
return [5, 6, 90].includes(orientation);
|
||||
}
|
||||
|
||||
function isRotated270CW(orientation: number) {
|
||||
return orientation === 7 || orientation === 8 || orientation === -90;
|
||||
return [7, 8, -90].includes(orientation);
|
||||
}
|
||||
|
||||
export function isFlipped(orientation?: string | null) {
|
||||
@@ -245,6 +245,7 @@ async function addSupportedMimeTypes(): Promise<void> {
|
||||
heicImg.src =
|
||||
'data:image/heic;base64,AAAAGGZ0eXBoZWljAAAAAG1pZjFoZWljAAABrW1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAHBpY3QAAAAAAAAAAAAAAAAAAAAADnBpdG0AAAAAAAIAAAAQaWRhdAAAAAAAAQABAAAAOGlsb2MBAAAAREAAAgABAAAAAAAAAc0AAQAAAAAAAAAsAAIAAQAAAAAAAAABAAAAAAAAAAgAAAA4aWluZgAAAAAAAgAAABVpbmZlAgAAAQABAABodmMxAAAAABVpbmZlAgAAAAACAABncmlkAAAAANhpcHJwAAAAtmlwY28AAAB2aHZjQwEDcAAAAAAAAAAAAB7wAPz9+PgAAA8DIAABABhAAQwB//8DcAAAAwCQAAADAAADAB66AkAhAAEAKkIBAQNwAAADAJAAAAMAAAMAHqAggQWW6q6a5uBAQMCAAAADAIAAAAMAhCIAAQAGRAHBc8GJAAAAFGlzcGUAAAAAAAAAAQAAAAEAAAAUaXNwZQAAAAAAAABAAAAAQAAAABBwaXhpAAAAAAMICAgAAAAaaXBtYQAAAAAAAAACAAECgQMAAgIChAAAABppcmVmAAAAAAAAAA5kaW1nAAIAAQABAAAANG1kYXQAAAAoKAGvCchMZYA50NoPIfzz81Qfsm577GJt3lf8kLAr+NbNIoeRR7JeYA=='; // Small valid HEIC/HEIF image
|
||||
}
|
||||
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
||||
void addSupportedMimeTypes();
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,10 +28,12 @@ export const authenticate = async (url: URL, options?: AuthOptions) => {
|
||||
};
|
||||
|
||||
export const requestServerInfo = async () => {
|
||||
if (authManager.authenticated) {
|
||||
const data = await getStorage();
|
||||
userInteraction.serverInfo = data;
|
||||
if (!authManager.authenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await getStorage();
|
||||
userInteraction.serverInfo = data;
|
||||
};
|
||||
|
||||
export const getAccountAge = (): number => {
|
||||
|
||||
@@ -23,7 +23,7 @@ const byteUnits = [ByteUnit.B, ByteUnit.KiB, ByteUnit.MiB, ByteUnit.GiB, ByteUni
|
||||
export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, ByteUnit] {
|
||||
const magnitude = Math.floor(Math.log(bytes === 0 ? 1 : bytes) / Math.log(1024));
|
||||
|
||||
return [Number.parseFloat((bytes / 1024 ** magnitude).toFixed(maxPrecision)), byteUnits[magnitude]];
|
||||
return [Number((bytes / 1024 ** magnitude).toFixed(maxPrecision)), byteUnits[magnitude]];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,11 +41,12 @@ export class GCastDestination implements ICastDestination {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/no-global-object-property-assignment
|
||||
window['__onGCastApiAvailable'] = (isAvailable: boolean) => {
|
||||
resolve(isAvailable);
|
||||
};
|
||||
|
||||
if (!document.querySelector(`script[src="${FRAMEWORK_LINK}"]`)) {
|
||||
if (!document.querySelector(`script[src="${CSS.escape(FRAMEWORK_LINK)}"]`)) {
|
||||
const script = document.createElement('script');
|
||||
script.src = FRAMEWORK_LINK;
|
||||
document.body.append(script);
|
||||
|
||||
@@ -27,23 +27,23 @@ export const getShortDateRange = (startTimestamp: string, endTimestamp: string)
|
||||
// Same year and month.
|
||||
// e.g.: aug. 2024
|
||||
return endDateLocalized;
|
||||
} else {
|
||||
// Same year but different month.
|
||||
// e.g.: jul. - sept. 2024
|
||||
const startMonthLocalized = startDate.toLocaleString({
|
||||
month: 'short',
|
||||
});
|
||||
return `${startMonthLocalized} - ${endDateLocalized}`;
|
||||
}
|
||||
} else {
|
||||
// Different year.
|
||||
// e.g.: feb. 2021 - sept. 2024
|
||||
const startDateLocalized = startDate.toLocaleString({
|
||||
|
||||
// Same year but different month.
|
||||
// e.g.: jul. - sept. 2024
|
||||
const startMonthLocalized = startDate.toLocaleString({
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
});
|
||||
return `${startDateLocalized} - ${endDateLocalized}`;
|
||||
return `${startMonthLocalized} - ${endDateLocalized}`;
|
||||
}
|
||||
|
||||
// Different year.
|
||||
// e.g.: feb. 2021 - sept. 2024
|
||||
const startDateLocalized = startDate.toLocaleString({
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
});
|
||||
return `${startDateLocalized} - ${endDateLocalized}`;
|
||||
};
|
||||
|
||||
const formatDate = (date?: string) => {
|
||||
|
||||
@@ -209,7 +209,7 @@ const metadataFields = [
|
||||
icon: mdiPhoneRotateLandscape,
|
||||
titleKey: 'orientation',
|
||||
keys: ['orientation'],
|
||||
render: (asset, $t) => String(asset.exifInfo?.orientation || $t('unknown')),
|
||||
render: (asset, $t) => asset.exifInfo?.orientation || $t('unknown'),
|
||||
},
|
||||
{
|
||||
icon: mdiPanorama,
|
||||
@@ -240,7 +240,7 @@ const normalizeForComparison = (key: MetadataFieldKey, value: unknown): unknown
|
||||
return value;
|
||||
}
|
||||
|
||||
if (key === 'fileCreatedAt' || key === 'fileModifiedAt' || key === 'dateTimeOriginal' || key === 'modifyDate') {
|
||||
if (['fileCreatedAt', 'fileModifiedAt', 'dateTimeOriginal', 'modifyDate'].includes(key)) {
|
||||
const dateTime = DateTime.fromISO(String(value));
|
||||
return dateTime.isValid ? dateTime.toISO() : String(value);
|
||||
}
|
||||
@@ -273,7 +273,7 @@ const getValueForAsset = (asset: AssetResponseDto, key: MetadataFieldKey): unkno
|
||||
return getAssetResolution(asset);
|
||||
}
|
||||
default: {
|
||||
if (asset.exifInfo && key in asset.exifInfo) {
|
||||
if (asset.exifInfo && Object.hasOwn(asset.exifInfo, key)) {
|
||||
return asset.exifInfo[key as keyof typeof asset.exifInfo];
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -131,7 +131,7 @@ export const fileUploadHandler = async ({
|
||||
};
|
||||
|
||||
function getDeviceAssetId(asset: File) {
|
||||
return 'web' + '-' + asset.name + '-' + asset.lastModified;
|
||||
return 'web-' + asset.name + '-' + asset.lastModified;
|
||||
}
|
||||
|
||||
function hashFile(file: File): Promise<string> {
|
||||
|
||||
@@ -47,7 +47,7 @@ export function handleError(error: unknown, localizedMessage: string, options?:
|
||||
try {
|
||||
let serverMessage = getServerErrorMessage(error);
|
||||
if (serverMessage) {
|
||||
serverMessage = `${String(serverMessage).slice(0, 75)}\n(Immich Server Error)`;
|
||||
serverMessage = `${serverMessage.slice(0, 75)}\n(Immich Server Error)`;
|
||||
}
|
||||
|
||||
const errorMessage = serverMessage || localizedMessage;
|
||||
|
||||
@@ -30,10 +30,7 @@ export class InvocationTracker {
|
||||
* @throws {Error} If the invocation is no longer valid
|
||||
*/
|
||||
isStillValid: () => {
|
||||
if (invocation !== this.invocationsStarted) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return invocation === this.invocationsStarted;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ export type AssetGridRouteSearchParams = {
|
||||
at: string | null | undefined;
|
||||
};
|
||||
export const isExternalUrl = (url: string): boolean => {
|
||||
return new URL(url, globalThis.location.href).origin !== globalThis.location.origin;
|
||||
return new URL(url, location.href).origin !== location.origin;
|
||||
};
|
||||
|
||||
export const isPhotosRoute = (route?: string | null) => !!route?.startsWith('/(user)/photos/[[assetId=id]]');
|
||||
@@ -33,11 +33,10 @@ function currentUrlWithoutAsset() {
|
||||
// off / instead of a subpath, unlike every other asset-containing route.
|
||||
if (isPhotosRoute(page.route.id)) {
|
||||
return Route.photos() + page.url.search;
|
||||
} else if (isSharedLinkSlugRoute(page.route.id)) {
|
||||
return Route.viewSharedLink({ slug: page.data.slug, key: page.data.key }) + page.url.search;
|
||||
} else {
|
||||
return page.url.pathname.replace(/(\/photos.*)$/, '') + page.url.search;
|
||||
}
|
||||
return isSharedLinkSlugRoute(page.route.id)
|
||||
? Route.viewSharedLink({ slug: page.data.slug, key: page.data.key }) + page.url.search
|
||||
: page.url.pathname.replace(/(\/photos.*)$/, '') + page.url.search;
|
||||
}
|
||||
|
||||
export function currentUrlReplaceAssetId(assetId: string) {
|
||||
@@ -133,7 +132,8 @@ async function navigateAssetGridRoute(route: AssetGridRoute, options?: NavOption
|
||||
export function navigate(change: ImmichRoute, options?: NavOptions): Promise<void> {
|
||||
if (isAssetGridRoute(change)) {
|
||||
return navigateAssetGridRoute(change, options);
|
||||
} else if (isAssetRoute(change)) {
|
||||
}
|
||||
if (isAssetRoute(change)) {
|
||||
return navigateAssetRoute(change, options);
|
||||
}
|
||||
// future navigation requests here
|
||||
@@ -141,20 +141,22 @@ export function navigate(change: ImmichRoute, options?: NavOptions): Promise<voi
|
||||
}
|
||||
|
||||
export const clearQueryParam = async (queryParam: string, url: URL) => {
|
||||
if (url.searchParams.has(queryParam)) {
|
||||
url.searchParams.delete(queryParam);
|
||||
await goto(url, { keepFocus: true });
|
||||
if (!url.searchParams.has(queryParam)) {
|
||||
return;
|
||||
}
|
||||
|
||||
url.searchParams.delete(queryParam);
|
||||
await goto(url, { keepFocus: true });
|
||||
};
|
||||
|
||||
export const getQueryValue = (queryKey: string) => {
|
||||
const url = globalThis.location.href;
|
||||
const url = location.href;
|
||||
const urlObject = new URL(url);
|
||||
return urlObject.searchParams.get(queryKey);
|
||||
};
|
||||
|
||||
export const setQueryValue = async (queryKey: string, queryValue: string) => {
|
||||
const url = globalThis.location.href;
|
||||
const url = location.href;
|
||||
const urlObject = new URL(url);
|
||||
urlObject.searchParams.set(queryKey, queryValue);
|
||||
await goto(urlObject, { keepFocus: true });
|
||||
|
||||
@@ -17,7 +17,7 @@ export type OcrBox = {
|
||||
};
|
||||
|
||||
const CJK_PATTERN =
|
||||
/[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uAC00-\uD7AF\uFF00-\uFFEF]/;
|
||||
/[\u{3000}-\u{303F}\u{3040}-\u{309F}\u{30A0}-\u{30FF}\u{3400}-\u{4DBF}\u{4E00}-\u{9FFF}\u{F900}-\u{FAFF}\u{AC00}-\u{D7AF}\u{FF00}-\u{FFEF}]/u;
|
||||
|
||||
const VERTICAL_ASPECT_RATIO = 1.5;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const removeAccents = (str: string) => {
|
||||
return str.normalize('NFD').replaceAll(/[\u0300-\u036F]/g, '');
|
||||
return str.normalize('NFD').replaceAll(/[\u{300}-\u{36F}]/gu, '');
|
||||
};
|
||||
|
||||
export const normalizeSearchString = (str: string) => {
|
||||
|
||||
@@ -95,8 +95,8 @@ export const fromTimelinePlainYearMonth = (timelineYearMonth: TimelineYearMonth)
|
||||
) as DateTime<true>;
|
||||
|
||||
export const toISOYearMonthUTC = ({ year, month }: TimelineYearMonth): string => {
|
||||
const yearFull = `${year}`.padStart(4, '0');
|
||||
const monthFull = `${month}`.padStart(2, '0');
|
||||
const yearFull = String(year).padStart(4, '0');
|
||||
const monthFull = String(month).padStart(2, '0');
|
||||
return `${yearFull}-${monthFull}-01T00:00:00.000Z`;
|
||||
};
|
||||
|
||||
|
||||
@@ -29,14 +29,16 @@ export async function acquireWakeLock() {
|
||||
}
|
||||
|
||||
export async function releaseWakeLock() {
|
||||
if (sentinel) {
|
||||
const toReleaseSentinel = sentinel;
|
||||
// Unset first to avoid race condition after await
|
||||
sentinel = undefined;
|
||||
|
||||
// eslint-disable-next-line tscompat/tscompat
|
||||
await toReleaseSentinel.release();
|
||||
if (!sentinel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toReleaseSentinel = sentinel;
|
||||
// Unset first to avoid race condition after await
|
||||
sentinel = undefined;
|
||||
|
||||
// eslint-disable-next-line tscompat/tscompat
|
||||
await toReleaseSentinel.release();
|
||||
}
|
||||
|
||||
if (isSupported) {
|
||||
|
||||
+11
-7
@@ -178,11 +178,13 @@
|
||||
};
|
||||
|
||||
const updateThumbnailUsingCurrentSelection = async () => {
|
||||
if (assetMultiSelectManager.assets.length === 1) {
|
||||
const [firstAsset] = assetMultiSelectManager.assets;
|
||||
assetMultiSelectManager.clear();
|
||||
await updateThumbnail(firstAsset.id);
|
||||
if (assetMultiSelectManager.assets.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [firstAsset] = assetMultiSelectManager.assets;
|
||||
assetMultiSelectManager.clear();
|
||||
await updateThumbnail(firstAsset.id);
|
||||
};
|
||||
|
||||
const updateThumbnail = async (assetId: string) => {
|
||||
@@ -274,10 +276,12 @@
|
||||
};
|
||||
|
||||
const onAlbumDelete = async ({ id }: AlbumResponseDto) => {
|
||||
if (id === album.id) {
|
||||
await goto(Route.albums());
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
if (id !== album.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
await goto(Route.albums());
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
};
|
||||
|
||||
const onAlbumAddAssets = async ({ albumIds }: { albumIds: string[] }) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user