chore(deps): update dependency eslint-plugin-unicorn to v78 (#32588)

Co-authored-by: Jason Rasmussen <jason@rasm.me>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit 72e2bc8b0bc4a7e0a84ac16c0bde7f5e538729fb)
This commit is contained in:
renovate[bot]
2026-10-28 20:41:28 +03:00
committed by Codex
parent 3127dbec71
commit 7fb7699c06
141 changed files with 654 additions and 761 deletions
+1 -1
View File
@@ -110,7 +110,7 @@
"eslint-plugin-compat": "^7.0.0",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-svelte": "^3.23.1",
"eslint-plugin-unicorn": "^75.0.0",
"eslint-plugin-unicorn": "^78.0.0",
"factory.ts": "^1.4.1",
"globals": "^17.0.0",
"happy-dom": "^21.0.0",
+5 -5
View File
@@ -7,11 +7,11 @@ interface Options {
}
/**
* Calls a function when a click occurs outside of the element, or when the escape key is pressed.
* @param node
* @param options Object containing onOutclick and onEscape functions
* @returns
*/
Calls a function when a click occurs outside of the element, or when the escape key is pressed.
@param node
@param options Object containing onOutclick and onEscape functions
@returns
*/
export function clickOutside(node: HTMLElement, options: Options = {}): ActionReturn<Options> {
const handleClick = (event: MouseEvent) => {
const { onOutclick } = options;
+7 -21
View File
@@ -3,33 +3,19 @@ import { tick } from 'svelte';
import type { Action } from 'svelte/action';
interface Options {
/**
* A function that is called when the dropdown should be closed.
*/
/** A function that is called when the dropdown should be closed. */
closeDropdown: () => void;
/**
* The container element that with direct children that should be navigated.
*/
/** The container element that with direct children that should be navigated. */
container?: HTMLElement;
/**
* Indicates if the dropdown is open.
*/
/** Indicates if the dropdown is open. */
isOpen: boolean;
/**
* Override the default behavior for the escape key.
*/
/** Override the default behavior for the escape key. */
onEscape?: (event: KeyboardEvent) => void;
/**
* A function that is called when the dropdown should be opened.
*/
/** A function that is called when the dropdown should be opened. */
openDropdown?: (event: KeyboardEvent) => void;
/**
* The id of the currently selected element.
*/
/** The id of the currently selected element. */
selectedId: string | undefined;
/**
* A function that is called when the selection changes, to notify consumers of the new selected id.
*/
/** A function that is called when the selection changes, to notify consumers of the new selected id. */
selectionChanged: (id: string | undefined) => void;
}
+4 -4
View File
@@ -5,10 +5,10 @@ interface Options {
}
/**
* Calls a function when focus leaves the element.
* @param node
* @param options Object containing onFocusOut function
*/
Calls a function when focus leaves the element.
@param node
@param options Object containing onFocusOut function
*/
export function focusOutside(node: HTMLElement, options: Options = {}) {
const { onFocusOut } = options;
+1 -3
View File
@@ -2,9 +2,7 @@ import { getTabbable } from '#lib/utils/focus-util.ts';
import { tick } from 'svelte';
interface Options {
/**
* Set whether the trap is active or not.
*/
/** Set whether the trap is active or not. */
active?: boolean;
}
+4 -4
View File
@@ -2,10 +2,10 @@ import { shortcuts } from '#lib/actions/shortcut.ts';
import type { Action } from 'svelte/action';
/**
* Enables keyboard navigation (up and down arrows) for a list of elements.
* @param node Element which listens for keyboard events
* @param container Element containing the list of elements
*/
Enables keyboard navigation (up and down arrows) for a list of elements.
@param node Element which listens for keyboard events
@param container Element containing the list of elements
*/
export const listNavigation: Action<HTMLElement, HTMLElement | undefined> = (
node: HTMLElement,
container?: HTMLElement,
+5 -15
View File
@@ -3,30 +3,20 @@ import { handlePromiseError } from '#lib/utils.ts';
import { navigating } from '$app/state';
interface Options {
/**
* This must be kept the same in all subpages of this route for the scroll memory clearer to work.
*/
/** This must be kept the same in all subpages of this route for the scroll memory clearer to work. */
routeStartsWith: string;
/**
* Function to clear additional data/state before scrolling (ex infinite scroll).
*/
/** Function to clear additional data/state before scrolling (ex infinite scroll). */
beforeClear?: () => void;
}
interface PageOptions extends Options {
/**
* Function to save additional data/state before scrolling (ex infinite scroll).
*/
/** Function to save additional data/state before scrolling (ex infinite scroll). */
beforeSave?: () => void;
/**
* Function to load additional data/state before scrolling (ex infinite scroll).
*/
/** Function to load additional data/state before scrolling (ex infinite scroll). */
beforeScroll?: () => Promise<void>;
}
/**
* @param node The scroll slot element, typically from {@link UserPageLayout}
*/
/** @param node The scroll slot element, typically from {@link UserPageLayout} */
export function scrollMemory(
node: HTMLElement,
{ routeStartsWith, beforeSave, beforeClear, beforeScroll }: PageOptions,
+3 -3
View File
@@ -1,7 +1,7 @@
/**
* @license Apache-2.0
* https://github.com/hperrin/svelte-material-ui/blob/master/packages/common/src/internal/useActions.ts
*/
@license Apache-2.0
https://github.com/hperrin/svelte-material-ui/blob/master/packages/common/src/internal/useActions.ts
*/
export type SvelteActionReturnType<P> = void | {
update?: (newParams?: P) => void;
@@ -7,13 +7,13 @@
variant?: Variants;
};
const { action, variant }: Props = $props();
const { action, variant = 'ghost' }: Props = $props();
const { title, icon, color = 'secondary', onAction } = $derived(action);
</script>
{#if action.$if?.() ?? true}
<Button
variant={variant ?? 'ghost'}
{variant}
size="small"
{color}
leadingIcon={icon}
@@ -302,9 +302,7 @@
cursor.current = stack.assets[nextIndex];
};
/**
* Slide show mode
*/
/** Slide show mode */
let assetViewerHtmlElement = $state<HTMLElement>();
@@ -23,7 +23,7 @@
break;
}
case CastState.IDLE: {
await cast(assetFileUrl, true);
await cast(true);
break;
}
default: {
@@ -35,7 +35,7 @@
$effect(() => {
if (assetFileUrl) {
void cast(assetFileUrl);
void cast();
}
});
@@ -47,11 +47,11 @@
previousPlayerState = castManager.castState;
});
const cast = async (url: string, force: boolean = false) => {
if (!url || !castManager.isCasting) {
const cast = async (force: boolean = false) => {
if (!assetFileUrl || !castManager.isCasting) {
return;
}
const fullUrl = new URL(url, location.href);
const fullUrl = new URL(assetFileUrl, location.href);
try {
await castManager.loadMedia(fullUrl.href, force);
@@ -4,7 +4,7 @@ import MediaTimeRange from 'media-chrome/media-time-range';
const COMMIT_DELAY_MS = 750;
/** Custom MediaTimeRange that only seeks after pointer release to avoid hammering the server.
* Keyboard input uses timed debouncing instead since there's no release event. */
Keyboard input uses timed debouncing instead since there's no release event. */
class ImmichTimeRange extends MediaTimeRange {
private seeking = false;
private pending: number | undefined;
@@ -36,13 +36,9 @@
options?: ComboBoxOption[];
selectedOption?: ComboBoxOption | undefined;
placeholder?: string;
/**
* whether creating new items is allowed.
*/
/** whether creating new items is allowed. */
allowCreate?: boolean;
/**
* select first matching option on enter key.
*/
/** select first matching option on enter key. */
defaultFirstOption?: boolean;
onSelect?: (option: ComboBoxOption | undefined) => void;
forceFocus?: boolean;
@@ -61,13 +57,9 @@
forceFocus = false,
}: Props = $props();
/**
* Indicates whether or not the dropdown autocomplete list should be visible.
*/
/** Indicates whether or not the dropdown autocomplete list should be visible. */
let isOpen = $state(false);
/**
* Keeps track of whether the combobox is actively being used.
*/
/** Keeps track of whether the combobox is actively being used. */
let isActive = $state(false);
let searchQuery = $derived(selectedOption?.label || '');
let selectedIndex: number | undefined = $state();
@@ -78,13 +70,9 @@
const id = $props.id();
const inputId = `combobox-${id}`;
const listboxId = `listbox-${id}`;
/**
* Buffer distance between the dropdown and top/bottom of the viewport.
*/
/** Buffer distance between the dropdown and top/bottom of the viewport. */
const dropdownOffset = 15;
/**
* Minimum space required for the dropdown to be displayed at the bottom of the input.
*/
/** Minimum space required for the dropdown to be displayed at the bottom of the input. */
const bottomBreakpoint = 225;
const observer = new IntersectionObserver(
(entries) => {
@@ -16,21 +16,15 @@
type Props = {
icon: string;
title: string;
/**
* The alignment of the context menu relative to the button.
*/
/** The alignment of the context menu relative to the button. */
align?: Align;
/**
* The direction in which the context menu should open.
*/
/** The direction in which the context menu should open. */
// TODO change to start vs end
direction?: 'left' | 'right';
color?: Color;
size?: Size | undefined;
variant?: Variants | undefined;
/**
* Additional classes to apply to the button.
*/
/** Additional classes to apply to the button. */
buttonClass?: string | undefined;
hideContent?: boolean;
children?: Snippet;
@@ -7,13 +7,11 @@
interface Props {
/**
* Autoplay on mount
* @default false
*/
Autoplay on mount
@default false
*/
autoplay?: boolean;
/**
* Progress bar status
*/
/** Progress bar status */
status?: ProgressBarStatus;
hidden?: boolean;
duration?: number;
@@ -36,8 +36,8 @@
isSelectionMode?: boolean;
singleSelect?: boolean;
/** `true` if this asset grid is responds to navigation events; if `true`, then look at the
`AssetViewingStore.gridScrollTarget` and load and scroll to the asset specified, and
additionally, update the page location/url with the asset as the timeline is scrolled */
`AssetViewingStore.gridScrollTarget` and load and scroll to the asset specified, and
additionally, update the page location/url with the asset as the timeline is scrolled */
enableRouting: boolean;
timelineManager?: TimelineManager;
options?: TimelineManagerOptions;
+2 -6
View File
@@ -2,9 +2,7 @@
import { handlePromiseError } from '#lib/utils.ts';
import { tick, type Snippet } from 'svelte';
/**
* Usage: <div use:portal={'css selector'}> or <div use:portal={document.body}>
*/
/** Usage: <div use:portal={'css selector'}> or <div use:portal={document.body}> */
export function portal(element: HTMLElement, target: HTMLElement | string = 'body') {
let targetElement;
async function update(newTarget: HTMLElement | string) {
@@ -65,9 +63,7 @@ Used for every occurrence of an HTML tag in a message
-->
<script lang="ts">
interface Props {
/**
* DOM Element or CSS Selector
*/
/** DOM Element or CSS Selector */
target?: HTMLElement | string;
children?: Snippet;
}
+3 -9
View File
@@ -4,17 +4,11 @@
import { t } from 'svelte-i18n';
interface Props {
/**
* Target for the skip link to move focus to.
*/
/** Target for the skip link to move focus to. */
target?: string;
/**
* Text for the skip link button.
*/
/** Text for the skip link button. */
text?: string;
/**
* Breakpoint at which the skip link is visible. Defaults to always being visible.
*/
/** Breakpoint at which the skip link is visible. Defaults to always being visible. */
breakpoint?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
}
@@ -54,8 +54,8 @@ class ActivityManager {
return this.#isLiked;
}
#getCacheKey(albumId: string, assetId?: string) {
return `${albumId}:${assetId ?? ''}`;
#getCacheKey(albumId: string, assetId: string = '') {
return `${albumId}:${assetId}`;
}
async init(albumId: string, assetId?: string) {
@@ -514,9 +514,7 @@ export class TimelineManager extends VirtualScrollManager {
return randomDay.viewerAssets[randomAssetIndex - accumulatedCount].asset;
}
/**
* Executes callback on assets, handling moves between groups and removals due to filter criteria.
*/
/** Executes callback on assets, handling moves between groups and removals due to filter criteria. */
update(ids: string[], callback: (asset: TimelineAsset) => void) {
// eslint-disable-next-line svelte/prefer-svelte-reactivity
return this.#runAssetCallback(new Set(ids), callback);
@@ -540,11 +538,11 @@ export class TimelineManager extends VirtualScrollManager {
}
/**
* Adds assets to existing segments, creating new segments as needed.
*
* This is an internal method that assumes the provided assets are not already
* present in the timeline. For updating existing assets, use updateAssetOperation().
*/
Adds assets to existing segments, creating new segments as needed.
This is an internal method that assumes the provided assets are not already
present in the timeline. For updating existing assets, use updateAssetOperation().
*/
protected addAssetsUpsertSegments(assets: TimelineAsset[]) {
if (assets.length === 0) {
return;
+23 -23
View File
@@ -2,38 +2,38 @@ import { DateTime, Duration } from 'luxon';
export type ZoneOption = {
/**
* Timezone name with offset
*
* e.g. Asia/Jerusalem (+03:00)
*/
Timezone name with offset
e.g. Asia/Jerusalem (+03:00)
*/
label: string;
/**
* Timezone name
*
* e.g. Asia/Jerusalem
*/
Timezone name
e.g. Asia/Jerusalem
*/
value: string;
/**
* Timezone offset in minutes
*
* e.g. 300
*/
Timezone offset in minutes
e.g. 300
*/
offsetMinutes: number;
/**
* True iff the date is valid
*
* Dates may be invalid for various reasons, for example setting a day that does not exist (30 Feb 2024).
* Due to daylight saving time, 2:30am is invalid for Europe/Berlin on Mar 31 2024.The two following local times
* are one second apart:
*
* - Mar 31 2024 01:59:59 (GMT+0100, unix timestamp 1725058799)
* - Mar 31 2024 03:00:00 (GMT+0200, unix timestamp 1711846800)
*
* Mar 31 2024 02:30:00 does not exist in Europe/Berlin, this is an invalid date/time/time zone combination.
*/
True iff the date is valid
Dates may be invalid for various reasons, for example setting a day that does not exist (30 Feb 2024).
Due to daylight saving time, 2:30am is invalid for Europe/Berlin on Mar 31 2024.The two following local times
are one second apart:
- Mar 31 2024 01:59:59 (GMT+0100, unix timestamp 1725058799)
- Mar 31 2024 03:00:00 (GMT+0200, unix timestamp 1711846800)
Mar 31 2024 02:30:00 does not exist in Europe/Berlin, this is an invalid date/time/time zone combination.
*/
valid: boolean;
};
+3 -7
View File
@@ -14,11 +14,7 @@ export const fromQueueSlug = (slug: string): QueueName | undefined => {
};
type QueryValue = number | string | boolean;
const asQueryString = (
params?: Record<string, QueryValue | undefined>,
options?: { skipEmptyStrings?: boolean; skipNullValues?: boolean },
) => {
const { skipEmptyStrings = true, skipNullValues = true } = options ?? {};
const asQueryString = (params?: Record<string, QueryValue | undefined>) => {
const items = Object.entries(params ?? {})
.filter((item): item is [string, QueryValue] => {
const value = item[1];
@@ -27,11 +23,11 @@ const asQueryString = (
return false;
}
if (skipNullValues && value === null) {
if (value === null) {
return false;
}
return !(skipEmptyStrings && value === '');
return value !== '';
})
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
+2 -2
View File
@@ -7,7 +7,7 @@ import { browser } from '$app/env';
export const locale = persisted('locale', 'default', {
serializer: {
parse: (text) => convertBCP47(text) || 'default',
stringify: (object) => object ?? '',
stringify: (object = '') => object,
},
});
@@ -15,7 +15,7 @@ const preferredLocale = browser ? getPreferredLocale() : undefined;
export const lang = persisted<string>('lang', preferredLocale || defaultLang.code, {
serializer: {
parse: (text) => convertBCP47(text),
stringify: (object) => object ?? '',
stringify: (object = '') => object,
},
});
+2 -6
View File
@@ -3,16 +3,12 @@ import { mediaQueryManager } from '#lib/stores/media-query-manager.svelte.ts';
class SidebarStore {
isOpen = $derived.by(() => mediaQueryManager.isFullSidebar);
/**
* Reset the sidebar visibility to the default, based on the current screen width.
*/
/** Reset the sidebar visibility to the default, based on the current screen width. */
reset() {
this.isOpen = mediaQueryManager.isFullSidebar;
}
/**
* Toggles the sidebar visibility, if available at the current screen width.
*/
/** Toggles the sidebar visibility, if available at the current screen width. */
toggle() {
this.isOpen = mediaQueryManager.isFullSidebar ? true : !this.isOpen;
}
+2 -2
View File
@@ -132,7 +132,7 @@ export const downloadRequest = <TBody = unknown>(options: DownloadRequestOptions
options = { url: options };
}
const { signal, method, url, data: body, onDownloadProgress: onProgress } = options;
const { signal, method = 'GET', url, data: body, onDownloadProgress: onProgress } = options;
return new Promise<{ data: Blob; status: number }>((resolve, reject) => {
const xhr = new XMLHttpRequest();
@@ -155,7 +155,7 @@ export const downloadRequest = <TBody = unknown>(options: DownloadRequestOptions
signal.addEventListener('abort', () => xhr.abort());
}
xhr.open(method || 'GET', url);
xhr.open(method, url);
xhr.responseType = 'blob';
if (body) {
+4 -4
View File
@@ -56,10 +56,10 @@ const undoDeleteAssets = async (onUndoDelete: OnUndoDelete, assets: TimelineAsse
};
/**
* Update the asset stack state in the asset store based on the provided stack response.
* This function updates the stack information so that the icon is shown for the primary asset
* and removes the non-primary assets from the timeline.
*/
Update the asset stack state in the asset store based on the provided stack response.
This function updates the stack information so that the icon is shown for the primary asset
and removes the non-primary assets from the timeline.
*/
export function updateStackedAssetInTimeline(timelineManager: TimelineManager, stack: StackResponseDto) {
timelineManager.update([stack.primaryAssetId], (asset) => {
asset.stack = {
+16 -16
View File
@@ -16,20 +16,20 @@ import { get } from 'svelte/store';
import { goto } from '$app/navigation';
/**
* -------------------------
* Albums General Management
* -------------------------
*/
-------------------------
Albums General Management
-------------------------
*/
export const createAlbumAndRedirect = (name?: string, assetIds?: string[]) =>
executeMutation(createAlbumMutation, [{ createAlbumDto: { albumName: name ?? '', assetIds } }], {
onSuccess: (album) => void goto(Route.viewAlbum(album)),
});
/**
* -------------
* Album Sorting
* -------------
*/
-------------
Album Sorting
-------------
*/
export interface AlbumSortOptionMetadata {
id: AlbumSortBy;
defaultOrder: SortOrder;
@@ -82,10 +82,10 @@ export const findFilterOption = (filter: string) => {
};
/**
* --------------
* Album Grouping
* --------------
*/
--------------
Album Grouping
--------------
*/
export interface AlbumGroup {
id: string;
name: string;
@@ -136,10 +136,10 @@ export const getSelectedAlbumGroupOption = (settings: AlbumViewSettings) => {
};
/**
* ----------------------------
* Album Groups Collapse/Expand
* ----------------------------
*/
----------------------------
Album Groups Collapse/Expand
----------------------------
*/
const getCollapsedAlbumGroups = (settings: AlbumViewSettings) => {
settings.collapsedGroups ??= {};
const { collapsedGroups, groupBy } = settings;
+6 -12
View File
@@ -100,18 +100,16 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
};
/**
* Returns the lowercase filename extension without a dot (.) and
* an empty string when not found.
*/
Returns the lowercase filename extension without a dot (.) and
an empty string when not found.
*/
export function getFilenameExtension(filename: string): string {
const lastIndex = Math.max(0, filename.lastIndexOf('.'));
const startIndex = (lastIndex || Infinity) + 1;
return filename.slice(startIndex).toLowerCase();
}
/**
* Returns the filename of an asset including file extension
*/
/** Returns the filename of an asset including file extension */
export function getAssetFilename(asset: AssetResponseDto): string {
const fileExtension = getFilenameExtension(asset.originalPath);
return `${asset.originalFileName}.${fileExtension}`;
@@ -152,9 +150,7 @@ export function getAssetResolution(asset: AssetResponseDto): string {
return `${asset.width} × ${asset.height}`;
}
/**
* Returns aspect ratio for the asset
*/
/** Returns aspect ratio for the asset */
export function getAssetRatio(asset: AssetResponseDto) {
return asset.width && asset.height ? asset.width / asset.height : null;
}
@@ -222,9 +218,7 @@ async function addSupportedMimeTypes(): Promise<void> {
// eslint-disable-next-line unicorn/no-top-level-side-effects
void addSupportedMimeTypes();
/**
* Returns true if the asset is an image supported by web browsers, false otherwise
*/
/** Returns true if the asset is an image supported by web browsers, false otherwise */
export function isWebCompatibleImage(asset: AssetResponseDto): boolean {
if (!asset.originalMimeType) {
return false;
+36 -36
View File
@@ -11,15 +11,15 @@ export const enum ByteUnit {
const byteUnits = [ByteUnit.B, ByteUnit.KiB, ByteUnit.MiB, ByteUnit.GiB, ByteUnit.TiB, ByteUnit.PiB, ByteUnit.EiB];
/**
* Convert bytes to best human readable unit and number of that unit.
*
* * For `1024` bytes, returns `1` and `KiB`.
* * For `1536` bytes, returns `1.5` and `KiB`.
*
* @param bytes number of bytes
* @param maxPrecision maximum number of decimal places, default is `1`
* @returns size (number) and unit (string)
*/
Convert bytes to best human readable unit and number of that unit.
* For `1024` bytes, returns `1` and `KiB`.
* For `1536` bytes, returns `1.5` and `KiB`.
@param bytes number of bytes
@param maxPrecision maximum number of decimal places, default is `1`
@returns size (number) and unit (string)
*/
export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, ByteUnit] {
const magnitude = Math.floor(Math.log(bytes === 0 ? 1 : bytes) / Math.log(1024));
@@ -27,44 +27,44 @@ export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, Byte
}
/**
* Localized number of bytes with a unit.
*
* For `1536` bytes:
* * en: `1.5 KiB`
* * de: `1,5 KiB`
*
* @param bytes number of bytes
* @param locale locale to use, default is `navigator.language`
* @param maxPrecision maximum number of decimal places, default is `1`
* @returns localized bytes with unit as string
*/
Localized number of bytes with a unit.
For `1536` bytes:
* en: `1.5 KiB`
* de: `1,5 KiB`
@param bytes number of bytes
@param locale locale to use, default is `navigator.language`
@param maxPrecision maximum number of decimal places, default is `1`
@returns localized bytes with unit as string
*/
export function getByteUnitString(bytes: number, locale?: string, maxPrecision = 1): string {
const [size, unit] = getBytesWithUnit(bytes, maxPrecision);
return `${size.toLocaleString(locale)} ${unit}`;
}
/**
* Convert to bytes from on a specified unit.
*
* * `1, 'GiB'`, returns `1073741824` bytes
*
* @param size value to be converted
* @param unit unit to convert from
* @returns bytes (number)
*/
Convert to bytes from on a specified unit.
* `1, 'GiB'`, returns `1073741824` bytes
@param size value to be converted
@param unit unit to convert from
@returns bytes (number)
*/
export function convertToBytes(size: number, unit: ByteUnit): number {
return size * 1024 ** byteUnits.indexOf(unit);
}
/**
* Convert from bytes to a specified unit.
*
* * `11073741824, 'GiB'`, returns `1` GiB
*
* @param bytes value to be converted
* @param unit unit to convert to
* @returns bytes (number)
*/
Convert from bytes to a specified unit.
* `11073741824, 'GiB'`, returns `1` GiB
@param bytes value to be converted
@param unit unit to convert to
@returns bytes (number)
*/
export function convertFromBytes(bytes: number, unit: ByteUnit): number {
return bytes / 1024 ** byteUnits.indexOf(unit);
}
+1 -3
View File
@@ -1,9 +1,7 @@
export class CancellableTask {
cancelToken: AbortController | null = null;
cancellable: boolean = true;
/**
* A promise that resolves once the bucket is loaded, and rejects if bucket is canceled.
*/
/** A promise that resolves once the bucket is loaded, and rejects if bucket is canceled. */
complete!: Promise<unknown>;
executed: boolean = false;
+28 -10
View File
@@ -19,11 +19,15 @@ describe('getShortDateRange', () => {
});
it('should correctly return month range if start and end date are in separate months within the same year', () => {
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('Jan – Feb 2022');
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual(
'Jan\u{2009}–\u{2009}Feb 2022',
);
});
it('should correctly return range if start and end date are in separate months and years', () => {
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 – Jan 2022');
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual(
'Dec 2021\u{2009}–\u{2009}Jan 2022',
);
});
it('should correctly return long month if start and end date are within the same month, ignoring local time zone', () => {
@@ -38,17 +42,23 @@ describe('getShortDateRange', () => {
it('should correctly return month range if start and end date are in separate months within the same year, ignoring local time zone', () => {
vi.stubEnv('TZ', 'UTC+6');
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('Jan – Feb 2022');
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual(
'Jan\u{2009}–\u{2009}Feb 2022',
);
});
it('should correctly return range if start and end date are in separate months and years, ignoring local time zone', () => {
vi.stubEnv('TZ', 'UTC+6');
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 – Jan 2022');
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual(
'Dec 2021\u{2009}–\u{2009}Jan 2022',
);
});
it('should correctly return range if start and end date are in separate months and years, ignoring local time zone', () => {
vi.stubEnv('TZ', 'UTC-6');
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 – Jan 2022');
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual(
'Dec 2021\u{2009}–\u{2009}Jan 2022',
);
});
it('should use the correct locale to return month range', () => {
@@ -58,7 +68,9 @@ describe('getShortDateRange', () => {
it('should use the correct locale to return month-year range', () => {
locale.set('fr');
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('déc. 2021 – janv. 2022');
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual(
'déc. 2021\u{2009}–\u{2009}janv. 2022',
);
});
});
@@ -72,7 +84,7 @@ describe('getAlbumDateRange', () => {
});
it('should work', () => {
expect(getAlbumDateRange('2021-01-01T00:00:00Z', '2021-01-05T00:00:00Z')).toEqual('Jan 1 – 5, 2021');
expect(getAlbumDateRange('2021-01-01T00:00:00Z', '2021-01-05T00:00:00Z')).toEqual('Jan 1\u{2009}–\u{2009}5, 2021');
});
it('should work with a single day range', () => {
@@ -81,17 +93,23 @@ describe('getAlbumDateRange', () => {
it('should use the proper locale', () => {
locale.set('fr');
expect(getAlbumDateRange('2020-03-26T12:00:00Z', '2021-12-01T00:00:00Z')).toEqual('26 mars 2020 – 1 déc. 2021');
expect(getAlbumDateRange('2020-03-26T12:00:00Z', '2021-12-01T00:00:00Z')).toEqual(
'26 mars 2020\u{2009}–\u{2009}1 déc. 2021',
);
locale.set('en');
});
it('should correctly return range if start and end date are in separate months and years, ignoring local time zone', () => {
vi.stubEnv('TZ', 'UTC+6');
expect(getAlbumDateRange('2021-12-01T00:00:00Z', '2022-01-01T00:00:00Z')).toEqual('Dec 1, 2021 – Jan 1, 2022');
expect(getAlbumDateRange('2021-12-01T00:00:00Z', '2022-01-01T00:00:00Z')).toEqual(
'Dec 1, 2021\u{2009}–\u{2009}Jan 1, 2022',
);
});
it('should correctly return range if start and end date are in separate months and years, ignoring local time zone', () => {
vi.stubEnv('TZ', 'UTC-6');
expect(getAlbumDateRange('2021-12-01T00:00:00Z', '2022-01-01T00:00:00Z')).toEqual('Dec 1, 2021 – Jan 1, 2022');
expect(getAlbumDateRange('2021-12-01T00:00:00Z', '2022-01-01T00:00:00Z')).toEqual(
'Dec 1, 2021\u{2009}–\u{2009}Jan 1, 2022',
);
});
});
+8 -10
View File
@@ -26,20 +26,18 @@ const getDateRange = (startTimestamp: string, endTimestamp: string, format: 'sho
};
/**
* Get localized date range in short format like 'Oct – Nov 2026', with full month if start and end are the same: 'October 2026'.
* Timestamps are expected to be date-only in UTC.
*/
Get localized date range in short format like 'Oct – Nov 2026', with full month if start and end are the same: 'October 2026'.
Timestamps are expected to be date-only in UTC.
*/
export const getShortDateRange = (start: string, end: string) => getDateRange(start, end, 'short');
/**
* Get localized date range in long format. Timestamps are expected to be date-only in UTC.
*/
/** Get localized date range in long format. Timestamps are expected to be date-only in UTC. */
export const getAlbumDateRange = (start: string, end: string) => getDateRange(start, end, 'long');
/**
* Use this to convert from "5pm EST" to "5pm UTC"
*
* Useful with some APIs where you want to query by "today", but the values in the database are stored as UTC
*/
Use this to convert from "5pm EST" to "5pm UTC"
Useful with some APIs where you want to query by "today", but the values in the database are stored as UTC
*/
export const asLocalTimeISO = (date: DateTime<true>) =>
(date.setZone('utc', { keepLocalTime: true }) as DateTime<true>).toISO();
+3 -3
View File
@@ -150,9 +150,9 @@ const metadataFields = [
keys: ['latitude', 'longitude'],
render: (asset, $t) =>
// eslint-disable-next-line eqeqeq
asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null
? `${asset.exifInfo.latitude.toFixed(4)}, ${asset.exifInfo.longitude.toFixed(4)}`
: $t('unknown'),
asset.exifInfo?.latitude == null || asset.exifInfo?.longitude == null
? $t('unknown')
: `${asset.exifInfo.latitude.toFixed(4)}, ${asset.exifInfo.longitude.toFixed(4)}`,
},
{
icon: mdiCameraOutline,
+1
View File
@@ -2,6 +2,7 @@ import type { EditActions } from '#lib/managers/edit/edit-manager.svelte.ts';
import type { MirrorParameters, RotateParameters } from '@immich/sdk';
import { compose, flipX, flipY, identity, rotate } from 'transformation-matrix';
// eslint-disable-next-line unicorn/no-unnecessary-parameters
const isCloseToZero = (x: number, epsilon: number = 1e-15) => Math.abs(x) < epsilon;
export const normalizeTransformEdits = (
+17 -19
View File
@@ -1,10 +1,10 @@
import { handleError } from '#lib/utils/handle-error.ts';
/**
* Tracks the state of asynchronous invocations to handle race conditions and stale operations.
* This class helps manage concurrent operations by tracking which invocations are active
* and allowing operations to check if they're still valid.
*/
Tracks the state of asynchronous invocations to handle race conditions and stale operations.
This class helps manage concurrent operations by tracking which invocations are active
and allowing operations to check if they're still valid.
*/
export class InvocationTracker {
/** Counter for the number of invocations that have been started */
invocationsStarted = 0;
@@ -14,28 +14,26 @@ export class InvocationTracker {
constructor() {}
/**
* Starts a new invocation and returns an object with utilities to manage the invocation lifecycle.
* @returns An object containing methods to manage the invocation:
* - isInvalidInvocationError: Checks if an error is an invalid invocation error
* - checkStillValid: Throws an error if the invocation is no longer valid
* - endInvocation: Marks the invocation as complete
*/
Starts a new invocation and returns an object with utilities to manage the invocation lifecycle.
@returns An object containing methods to manage the invocation:
- isInvalidInvocationError: Checks if an error is an invalid invocation error
- checkStillValid: Throws an error if the invocation is no longer valid
- endInvocation: Marks the invocation as complete
*/
startInvocation() {
this.invocationsStarted++;
const invocation = this.invocationsStarted;
return {
/**
* Throws an error if this invocation is no longer valid
* @throws {Error} If the invocation is no longer valid
*/
Throws an error if this invocation is no longer valid
@throws {Error} If the invocation is no longer valid
*/
isStillValid: () => {
return invocation === this.invocationsStarted;
},
/**
* Marks this invocation as complete
*/
/** Marks this invocation as complete */
endInvocation: () => {
this.invocationsEnded = invocation;
},
@@ -43,9 +41,9 @@ export class InvocationTracker {
}
/**
* Checks if there are any active invocations
* @returns True if there are active invocations, false otherwise
*/
Checks if there are any active invocations
@returns True if there are active invocations, false otherwise
*/
isActive() {
return this.invocationsStarted !== this.invocationsEnded;
}
+1 -3
View File
@@ -70,9 +70,7 @@ function currentUrl() {
}
interface Route {
/**
* The route to target, or 'current' to stay on current route.
*/
/** The route to target, or 'current' to stay on current route. */
targetRoute: string | 'current';
}
+4 -4
View File
@@ -31,10 +31,10 @@ const getVerticalMode = (width: number, height: number, text: string): VerticalM
};
/**
* Calculate bounding box transform from OCR points. Result matrix can be used as input for css matrix3d.
* @param points - Array of 4 corner points of the bounding box
* @returns 4x4 matrix to transform the div with text onto the polygon defined by the corner points, and size to set on the source div.
*/
Calculate bounding box transform from OCR points. Result matrix can be used as input for css matrix3d.
@param points - Array of 4 corner points of the bounding box
@returns 4x4 matrix to transform the div with text onto the polygon defined by the corner points, and size to set on the source div.
*/
export const calculateBoundingBoxMatrix = (points: Point[]): Size & { matrix: number[] } => {
const [topLeft, topRight, bottomRight, bottomLeft] = points;
+8 -8
View File
@@ -3,10 +3,10 @@ import { type AssetResponseDto } from '@immich/sdk';
import { get } from 'svelte/store';
/**
* --------------
* Places Grouping
* --------------
*/
--------------
Places Grouping
--------------
*/
export interface PlacesGroup {
id: string;
name: string;
@@ -14,10 +14,10 @@ export interface PlacesGroup {
}
/**
* ----------------------------
* Places Groups Collapse/Expand
* ----------------------------
*/
----------------------------
Places Groups Collapse/Expand
----------------------------
*/
const getCollapsedPlacesGroups = (settings: PlacesViewSettings) => {
settings.collapsedGroups ??= {};
const { collapsedGroups, groupBy } = settings;
+1 -3
View File
@@ -5,9 +5,7 @@ export class ServiceWorkerMessenger {
this.#serviceWorker = serviceWorker;
}
/**
* Send a one-way message to the service worker.
*/
/** Send a one-way message to the service worker. */
send(type: string, data: Record<string, unknown>) {
this.#serviceWorker.controller?.postMessage({
type,
@@ -88,12 +88,12 @@
let suggestedPeople: PersonResponseDto[] = $state([]);
/**
* Save the word used to search people name: for example,
* if searching 'r' and the server returns 15 people with names starting with 'r',
* there's no need to search again people with name starting with 'ri'.
* However, it needs to make a new api request if searching 'r' returns 20 names (arbitrary value, the limit sent back by the server).
* or if the new search word starts with another word / letter
**/
Save the word used to search people name: for example,
if searching 'r' and the server returns 15 people with names starting with 'r',
there's no need to search again people with name starting with 'ri'.
However, it needs to make a new api request if searching 'r' returns 20 names (arbitrary value, the limit sent back by the server).
or if the new search word starts with another word / letter
*/
let isSearchingPeople = $state(false);
let suggestionContainer: HTMLElement | undefined = $state();
@@ -16,7 +16,7 @@
let now = DateTime.now();
let expiresAt = $derived(sharedLink.expiresAt ? DateTime.fromISO(sharedLink.expiresAt) : undefined);
let isExpired = $derived(expiresAt ? now > expiresAt : false);
let isExpired = $derived(expiresAt && now > expiresAt);
const getCountDownExpirationDate = (expiresAtDate: DateTime, now: DateTime) => {
const relativeUnits: ToRelativeUnit[] = ['days', 'hours', 'minutes', 'seconds'];
@@ -114,9 +114,9 @@
},
});
const { success, error, errorMessage } = response[0];
const { success, error, errorMessage = error } = response[0];
if (!success) {
throw new Error(errorMessage || error);
throw new Error(errorMessage);
}
duplicates = duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
@@ -51,6 +51,7 @@
let dragImage = $state<Element>();
let isDropTarget = $state(false);
// eslint-disable-next-line unicorn/no-unnecessary-parameters
const truncate = (input: string, max = 24) => (input.length > max ? input.slice(0, max - 1) + '…' : input);
const formatConfigValue = (value: unknown): string => {
+5 -5
View File
@@ -26,11 +26,11 @@
let { uploadAsset }: Props = $props();
const handleDismiss = (uploadAsset: UploadAsset) => {
const handleDismiss = () => {
uploadAssetsStore.removeItem(uploadAsset.id);
};
const handleRetry = async (uploadAsset: UploadAsset) => {
const handleRetry = async () => {
uploadAssetsStore.removeItem(uploadAsset.id);
await fileUploadHandler({ files: [uploadAsset.file], albumId: uploadAsset.albumId });
};
@@ -76,16 +76,16 @@
>
<Icon icon={mdiOpenInNew} size="20" />
</a>
<button type="button" onclick={() => handleDismiss(uploadAsset)} class="" aria-hidden="true" tabindex={-1}>
<button type="button" onclick={() => handleDismiss()} class="" aria-hidden="true" tabindex={-1}>
<Icon icon={mdiClose} size="20" />
</button>
</div>
{:else if uploadAsset.state === UploadState.ERROR}
<div class="flex items-center justify-between gap-1">
<button type="button" onclick={() => handleRetry(uploadAsset)} class="" aria-hidden="true" tabindex={-1}>
<button type="button" onclick={() => handleRetry()} class="" aria-hidden="true" tabindex={-1}>
<Icon icon={mdiRestart} size="20" />
</button>
<button type="button" onclick={() => handleDismiss(uploadAsset)} class="" aria-hidden="true" tabindex={-1}>
<button type="button" onclick={() => handleDismiss()} class="" aria-hidden="true" tabindex={-1}>
<Icon icon={mdiClose} size="20" />
</button>
</div>
@@ -36,6 +36,7 @@
const [storage, storageUnit] = $derived(stats ? getStorageUsageWithUnit(stats.usage) : [undefined, undefined]);
// eslint-disable-next-line unicorn/no-unnecessary-parameters
const zeros = (value: number, maxLength = 13) => {
const valueLength = value.toString().length;
const zeroLength = maxLength - valueLength;
+5 -9
View File
@@ -106,25 +106,21 @@
authManager.user.isAdmin && !serverConfig.isOnboarded ? OnboardingRole.SERVER : OnboardingRole.USER,
);
let onboardingStepCount = $derived(onboardingSteps.filter((step) => shouldRunStep(step.role, userRole)).length);
let onboardingStepCount = $derived(onboardingSteps.filter((step) => shouldRunStep(step.role)).length);
let onboardingProgress = $derived(
onboardingSteps.filter((step, i) => shouldRunStep(step.role, userRole) && i <= index).length - 1,
onboardingSteps.filter((step, i) => shouldRunStep(step.role) && i <= index).length - 1,
);
const shouldRunStep = (stepRole: OnboardingRole, userRole: OnboardingRole) => {
const shouldRunStep = (stepRole: OnboardingRole) => {
return (
stepRole === OnboardingRole.USER ||
(stepRole === OnboardingRole.SERVER && userRole === OnboardingRole.SERVER && !serverConfig.isOnboarded)
);
};
const previousStepIndex = $derived(
onboardingSteps.findLastIndex((step, i) => shouldRunStep(step.role, userRole) && i < index),
);
const previousStepIndex = $derived(onboardingSteps.findLastIndex((step, i) => shouldRunStep(step.role) && i < index));
const nextStepIndex = $derived(
onboardingSteps.findIndex((step, i) => shouldRunStep(step.role, userRole) && i > index),
);
const nextStepIndex = $derived(onboardingSteps.findIndex((step, i) => shouldRunStep(step.role) && i > index));
const handleNextClicked = async () => {
if (nextStepIndex === -1) {