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:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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)}`);
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
+6
-6
@@ -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'];
|
||||
|
||||
+2
-2
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user