Merge remote-tracking branch 'origin/main' into lighter_buckets_web
This commit is contained in:
@@ -67,7 +67,7 @@
|
||||
</script>
|
||||
|
||||
<div in:fly={{ y: 10, duration: 200 }} class="absolute top-0 w-full z-[100] bg-transparent">
|
||||
<div
|
||||
<nav
|
||||
id="asset-selection-app-bar"
|
||||
class={[
|
||||
'grid',
|
||||
@@ -94,5 +94,5 @@
|
||||
<div class="me-4 flex place-items-center gap-1 justify-self-end">
|
||||
{@render trailing?.()}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { deleteAssets } from '$lib/utils/actions';
|
||||
import { archiveAssets, cancelMultiselect } from '$lib/utils/asset-utils';
|
||||
import { focusNext } from '$lib/utils/focus-util';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getJustifiedLayoutFromAssets, type CommonJustifiedLayout } from '$lib/utils/layout-utils';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
@@ -267,25 +268,8 @@
|
||||
}
|
||||
};
|
||||
|
||||
const focusNextAsset = () => {
|
||||
if (assetInteraction.focussedAssetId === null && assets.length > 0) {
|
||||
assetInteraction.focussedAssetId = assets[0].id;
|
||||
} else if (assetInteraction.focussedAssetId !== null && assets.length > 0) {
|
||||
const currentIndex = assets.findIndex((a) => a.id === assetInteraction.focussedAssetId);
|
||||
if (currentIndex !== -1 && currentIndex + 1 < assets.length) {
|
||||
assetInteraction.focussedAssetId = assets[currentIndex + 1].id;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const focusPreviousAsset = () => {
|
||||
if (assetInteraction.focussedAssetId !== null && assets.length > 0) {
|
||||
const currentIndex = assets.findIndex((a) => a.id === assetInteraction.focussedAssetId);
|
||||
if (currentIndex >= 1) {
|
||||
assetInteraction.focussedAssetId = assets[currentIndex - 1].id;
|
||||
}
|
||||
}
|
||||
};
|
||||
const focusNextAsset = () => focusNext((element) => element.dataset.thumbnailFocusContainer !== undefined, true);
|
||||
const focusPreviousAsset = () => focusNext((element) => element.dataset.thumbnailFocusContainer !== undefined, false);
|
||||
|
||||
let shortcutList = $derived(
|
||||
(() => {
|
||||
@@ -502,7 +486,6 @@
|
||||
asset={toTimelineAsset(asset)}
|
||||
selected={assetInteraction.hasSelectedAsset(asset.id)}
|
||||
selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)}
|
||||
focussed={assetInteraction.isFocussedAsset(asset.id)}
|
||||
thumbnailWidth={layout.width}
|
||||
thumbnailHeight={layout.height}
|
||||
/>
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { Theme } from '$lib/constants';
|
||||
import { colorTheme, mapSettings } from '$lib/stores/preferences.store';
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
import { mapSettings } from '$lib/stores/preferences.store';
|
||||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||
import { type MapMarkerResponseDto } from '@immich/sdk';
|
||||
import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js?url';
|
||||
import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js';
|
||||
import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson';
|
||||
import { type GeoJSONSource, GlobeControl, type LngLatLike } from 'maplibre-gl';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import maplibregl, { GlobeControl, type GeoJSONSource, type LngLatLike } from 'maplibre-gl';
|
||||
import { t } from 'svelte-i18n';
|
||||
import {
|
||||
AttributionControl,
|
||||
@@ -68,7 +68,7 @@
|
||||
let map: maplibregl.Map | undefined = $state();
|
||||
let marker: maplibregl.Marker | null = null;
|
||||
|
||||
const theme = $derived($mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT);
|
||||
const theme = $derived($mapSettings.allowDarkMode ? themeManager.value : Theme.LIGHT);
|
||||
const styleUrl = $derived(theme === Theme.DARK ? $serverConfig.mapDarkStyleUrl : $serverConfig.mapLightStyleUrl);
|
||||
|
||||
export function addClipMapMarker(lng: number, lat: number) {
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<HelpAndFeedbackModal onClose={() => (shouldShowHelpPanel = false)} {info} />
|
||||
{/if}
|
||||
|
||||
<section
|
||||
<nav
|
||||
id="dashboard-navbar"
|
||||
class="fixed z-[900] max-md:h-[var(--navbar-height-md)] h-[var(--navbar-height)] w-dvw text-sm"
|
||||
>
|
||||
@@ -209,4 +209,4 @@
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</nav>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import QRCode from 'qrcode';
|
||||
import { colorTheme } from '$lib/stores/preferences.store';
|
||||
import { Theme } from '$lib/constants';
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
import QRCode from 'qrcode';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
let promise = $derived(
|
||||
QRCode.toDataURL(value, {
|
||||
color: { dark: $colorTheme.value === Theme.DARK ? '#ffffffff' : '#000000ff', light: '#00000000' },
|
||||
color: { dark: themeManager.value === Theme.DARK ? '#ffffffff' : '#000000ff', light: '#00000000' },
|
||||
margin: 0,
|
||||
width,
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import type { AssetStore, LiteBucket } from '$lib/stores/assets-store.svelte';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { getFocusable } from '$lib/utils/focus-util';
|
||||
import { getTabbable } from '$lib/utils/focus-util';
|
||||
import { fromLocalDateTime, type ScrubberListener } from '$lib/utils/timeline-util';
|
||||
import { mdiPlay } from '@mdi/js';
|
||||
import { clamp } from 'lodash-es';
|
||||
@@ -376,7 +376,7 @@
|
||||
if (forward || backward) {
|
||||
event.preventDefault();
|
||||
|
||||
const focusable = getFocusable(document);
|
||||
const focusable = getTabbable(document.body);
|
||||
if (scrollBar) {
|
||||
const index = focusable.indexOf(scrollBar);
|
||||
if (index !== -1) {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import { onMount, tick, type Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import type { FormEventHandler } from 'svelte/elements';
|
||||
import { fly } from 'svelte/transition';
|
||||
import PasswordField from '../password-field.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { onMount, tick, type Snippet } from 'svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
|
||||
interface Props {
|
||||
inputType: SettingInputFieldType;
|
||||
value: string | number;
|
||||
value: string | number | undefined;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: string;
|
||||
@@ -147,7 +147,7 @@
|
||||
name={label}
|
||||
autocomplete={passwordAutocomplete}
|
||||
{required}
|
||||
password={value.toString()}
|
||||
password={(value || '').toString()}
|
||||
onInput={(passwordValue) => (value = passwordValue)}
|
||||
{disabled}
|
||||
{title}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { t } from 'svelte-i18n';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { mdiChevronDown } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
value: string | number;
|
||||
value: string | number | undefined;
|
||||
options: { value: string | number; text: string }[];
|
||||
label?: string;
|
||||
desc?: string;
|
||||
|
||||
@@ -7,14 +7,12 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SideBarSection>
|
||||
<nav aria-label={$t('primary')}>
|
||||
<SideBarLink title={$t('users')} routeId={AppRoute.ADMIN_USER_MANAGEMENT} icon={mdiAccountMultipleOutline} />
|
||||
<SideBarLink title={$t('jobs')} routeId={AppRoute.ADMIN_JOBS} icon={mdiSync} />
|
||||
<SideBarLink title={$t('settings')} routeId={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
|
||||
<SideBarLink title={$t('external_libraries')} routeId={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} />
|
||||
<SideBarLink title={$t('server_stats')} routeId={AppRoute.ADMIN_STATS} icon={mdiServer} />
|
||||
</nav>
|
||||
<SideBarSection ariaLabel={$t('primary')}>
|
||||
<SideBarLink title={$t('users')} routeId={AppRoute.ADMIN_USER_MANAGEMENT} icon={mdiAccountMultipleOutline} />
|
||||
<SideBarLink title={$t('jobs')} routeId={AppRoute.ADMIN_JOBS} icon={mdiSync} />
|
||||
<SideBarLink title={$t('settings')} routeId={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
|
||||
<SideBarLink title={$t('external_libraries')} routeId={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} />
|
||||
<SideBarLink title={$t('server_stats')} routeId={AppRoute.ADMIN_STATS} icon={mdiServer} />
|
||||
|
||||
<BottomInfo />
|
||||
</SideBarSection>
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
ariaLabel?: string;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
let { ariaLabel, children }: Props = $props();
|
||||
|
||||
const isHidden = $derived(!sidebarStore.isOpen && !mobileDevice.isFullSidebar);
|
||||
const isExpanded = $derived(sidebarStore.isOpen && !mobileDevice.isFullSidebar);
|
||||
@@ -30,8 +31,9 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<section
|
||||
<nav
|
||||
id="sidebar"
|
||||
aria-label={ariaLabel}
|
||||
tabindex="-1"
|
||||
class="immich-scrollbar relative z-10 w-0 sidebar:w-[16rem] overflow-y-auto overflow-x-hidden bg-immich-bg pt-8 transition-all duration-200 dark:bg-immich-dark-bg"
|
||||
class:shadow-2xl={isExpanded}
|
||||
@@ -46,4 +48,4 @@
|
||||
<div class="pe-6 flex flex-col gap-1 h-max min-h-full">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</section>
|
||||
</nav>
|
||||
|
||||
@@ -42,102 +42,100 @@
|
||||
let isUtilitiesSelected: boolean = $state(false);
|
||||
</script>
|
||||
|
||||
<SideBarSection>
|
||||
<nav aria-label={$t('primary')}>
|
||||
<SideBarSection ariaLabel={$t('primary')}>
|
||||
<SideBarLink
|
||||
title={$t('photos')}
|
||||
routeId="/(user)/photos"
|
||||
bind:isSelected={isPhotosSelected}
|
||||
icon={isPhotosSelected ? mdiImageMultiple : mdiImageMultipleOutline}
|
||||
></SideBarLink>
|
||||
|
||||
{#if $featureFlags.search}
|
||||
<SideBarLink title={$t('explore')} routeId="/(user)/explore" icon={mdiMagnify} />
|
||||
{/if}
|
||||
|
||||
{#if $featureFlags.map}
|
||||
<SideBarLink
|
||||
title={$t('photos')}
|
||||
routeId="/(user)/photos"
|
||||
bind:isSelected={isPhotosSelected}
|
||||
icon={isPhotosSelected ? mdiImageMultiple : mdiImageMultipleOutline}
|
||||
title={$t('map')}
|
||||
routeId="/(user)/map"
|
||||
bind:isSelected={isMapSelected}
|
||||
icon={isMapSelected ? mdiMap : mdiMapOutline}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if $preferences.people.enabled && $preferences.people.sidebarWeb}
|
||||
<SideBarLink
|
||||
title={$t('people')}
|
||||
routeId="/(user)/people"
|
||||
bind:isSelected={isPeopleSelected}
|
||||
icon={isPeopleSelected ? mdiAccount : mdiAccountOutline}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if $preferences.sharedLinks.enabled && $preferences.sharedLinks.sidebarWeb}
|
||||
<SideBarLink title={$t('shared_links')} routeId="/(user)/shared-links" icon={mdiLink} />
|
||||
{/if}
|
||||
|
||||
<SideBarLink
|
||||
title={$t('sharing')}
|
||||
routeId="/(user)/sharing"
|
||||
icon={isSharingSelected ? mdiAccountMultiple : mdiAccountMultipleOutline}
|
||||
bind:isSelected={isSharingSelected}
|
||||
></SideBarLink>
|
||||
|
||||
<p class="text-xs p-6 dark:text-immich-dark-fg">{$t('library').toUpperCase()}</p>
|
||||
|
||||
<SideBarLink
|
||||
title={$t('favorites')}
|
||||
routeId="/(user)/favorites"
|
||||
icon={isFavoritesSelected ? mdiHeart : mdiHeartOutline}
|
||||
bind:isSelected={isFavoritesSelected}
|
||||
></SideBarLink>
|
||||
|
||||
<SideBarLink
|
||||
title={$t('albums')}
|
||||
routeId="/(user)/albums"
|
||||
icon={mdiImageAlbum}
|
||||
flippedLogo
|
||||
bind:dropdownOpen={$recentAlbumsDropdown}
|
||||
>
|
||||
{#snippet dropDownContent()}
|
||||
<span in:fly={{ y: -20 }} class="hidden md:block">
|
||||
<RecentAlbums />
|
||||
</span>
|
||||
{/snippet}
|
||||
</SideBarLink>
|
||||
|
||||
{#if $preferences.tags.enabled && $preferences.tags.sidebarWeb}
|
||||
<SideBarLink title={$t('tags')} routeId="/(user)/tags" icon={mdiTagMultipleOutline} flippedLogo />
|
||||
{/if}
|
||||
|
||||
{#if $preferences.folders.enabled && $preferences.folders.sidebarWeb}
|
||||
<SideBarLink title={$t('folders')} routeId="/(user)/folders" icon={mdiFolderOutline} flippedLogo />
|
||||
{/if}
|
||||
|
||||
<SideBarLink
|
||||
title={$t('utilities')}
|
||||
routeId="/(user)/utilities"
|
||||
bind:isSelected={isUtilitiesSelected}
|
||||
icon={isUtilitiesSelected ? mdiToolbox : mdiToolboxOutline}
|
||||
></SideBarLink>
|
||||
|
||||
<SideBarLink
|
||||
title={$t('archive')}
|
||||
routeId="/(user)/archive"
|
||||
bind:isSelected={isArchiveSelected}
|
||||
icon={isArchiveSelected ? mdiArchiveArrowDown : mdiArchiveArrowDownOutline}
|
||||
></SideBarLink>
|
||||
|
||||
{#if $featureFlags.trash}
|
||||
<SideBarLink
|
||||
title={$t('trash')}
|
||||
routeId="/(user)/trash"
|
||||
bind:isSelected={isTrashSelected}
|
||||
icon={isTrashSelected ? mdiTrashCan : mdiTrashCanOutline}
|
||||
></SideBarLink>
|
||||
|
||||
{#if $featureFlags.search}
|
||||
<SideBarLink title={$t('explore')} routeId="/(user)/explore" icon={mdiMagnify} />
|
||||
{/if}
|
||||
|
||||
{#if $featureFlags.map}
|
||||
<SideBarLink
|
||||
title={$t('map')}
|
||||
routeId="/(user)/map"
|
||||
bind:isSelected={isMapSelected}
|
||||
icon={isMapSelected ? mdiMap : mdiMapOutline}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if $preferences.people.enabled && $preferences.people.sidebarWeb}
|
||||
<SideBarLink
|
||||
title={$t('people')}
|
||||
routeId="/(user)/people"
|
||||
bind:isSelected={isPeopleSelected}
|
||||
icon={isPeopleSelected ? mdiAccount : mdiAccountOutline}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if $preferences.sharedLinks.enabled && $preferences.sharedLinks.sidebarWeb}
|
||||
<SideBarLink title={$t('shared_links')} routeId="/(user)/shared-links" icon={mdiLink} />
|
||||
{/if}
|
||||
|
||||
<SideBarLink
|
||||
title={$t('sharing')}
|
||||
routeId="/(user)/sharing"
|
||||
icon={isSharingSelected ? mdiAccountMultiple : mdiAccountMultipleOutline}
|
||||
bind:isSelected={isSharingSelected}
|
||||
></SideBarLink>
|
||||
|
||||
<p class="text-xs p-6 dark:text-immich-dark-fg">{$t('library').toUpperCase()}</p>
|
||||
|
||||
<SideBarLink
|
||||
title={$t('favorites')}
|
||||
routeId="/(user)/favorites"
|
||||
icon={isFavoritesSelected ? mdiHeart : mdiHeartOutline}
|
||||
bind:isSelected={isFavoritesSelected}
|
||||
></SideBarLink>
|
||||
|
||||
<SideBarLink
|
||||
title={$t('albums')}
|
||||
routeId="/(user)/albums"
|
||||
icon={mdiImageAlbum}
|
||||
flippedLogo
|
||||
bind:dropdownOpen={$recentAlbumsDropdown}
|
||||
>
|
||||
{#snippet dropDownContent()}
|
||||
<span in:fly={{ y: -20 }} class="hidden md:block">
|
||||
<RecentAlbums />
|
||||
</span>
|
||||
{/snippet}
|
||||
</SideBarLink>
|
||||
|
||||
{#if $preferences.tags.enabled && $preferences.tags.sidebarWeb}
|
||||
<SideBarLink title={$t('tags')} routeId="/(user)/tags" icon={mdiTagMultipleOutline} flippedLogo />
|
||||
{/if}
|
||||
|
||||
{#if $preferences.folders.enabled && $preferences.folders.sidebarWeb}
|
||||
<SideBarLink title={$t('folders')} routeId="/(user)/folders" icon={mdiFolderOutline} flippedLogo />
|
||||
{/if}
|
||||
|
||||
<SideBarLink
|
||||
title={$t('utilities')}
|
||||
routeId="/(user)/utilities"
|
||||
bind:isSelected={isUtilitiesSelected}
|
||||
icon={isUtilitiesSelected ? mdiToolbox : mdiToolboxOutline}
|
||||
></SideBarLink>
|
||||
|
||||
<SideBarLink
|
||||
title={$t('archive')}
|
||||
routeId="/(user)/archive"
|
||||
bind:isSelected={isArchiveSelected}
|
||||
icon={isArchiveSelected ? mdiArchiveArrowDown : mdiArchiveArrowDownOutline}
|
||||
></SideBarLink>
|
||||
|
||||
{#if $featureFlags.trash}
|
||||
<SideBarLink
|
||||
title={$t('trash')}
|
||||
routeId="/(user)/trash"
|
||||
bind:isSelected={isTrashSelected}
|
||||
icon={isTrashSelected ? mdiTrashCan : mdiTrashCanOutline}
|
||||
></SideBarLink>
|
||||
{/if}
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<BottomInfo />
|
||||
</SideBarSection>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { moonPath, moonViewBox, sunPath, sunViewBox } from '$lib/assets/svg-paths';
|
||||
import CircleIconButton, { type Padding } from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { Theme } from '$lib/constants';
|
||||
import { colorTheme, handleToggleTheme } from '$lib/stores/preferences.store';
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let icon = $derived($colorTheme.value === Theme.LIGHT ? moonPath : sunPath);
|
||||
let viewBox = $derived($colorTheme.value === Theme.LIGHT ? moonViewBox : sunViewBox);
|
||||
let isDark = $derived($colorTheme.value === Theme.DARK);
|
||||
let icon = $derived(themeManager.isDark ? sunPath : moonPath);
|
||||
let viewBox = $derived(themeManager.isDark ? sunViewBox : moonViewBox);
|
||||
|
||||
interface Props {
|
||||
padding?: Padding;
|
||||
@@ -16,14 +14,14 @@
|
||||
let { padding = '3' }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if !$colorTheme.system}
|
||||
{#if !themeManager.theme.system}
|
||||
<CircleIconButton
|
||||
title={$t('toggle_theme')}
|
||||
{icon}
|
||||
{viewBox}
|
||||
role="switch"
|
||||
aria-checked={isDark ? 'true' : 'false'}
|
||||
onclick={handleToggleTheme}
|
||||
aria-checked={themeManager.isDark ? 'true' : 'false'}
|
||||
onclick={() => themeManager.toggleTheme()}
|
||||
{padding}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user