mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(web): assets now have a permanent URL (#8532)
* Remove asest redirect pages * Rename route paths to handle optional assetId * Update old references to new routes * Load and display asset from all routes that can show assetId * Add <main> in base layout, update portals to target it * Wire up updating navigation in response to open/close/prev/next * Replace events with navigation functions * Add types to param matcher * misc cleanup * Fix reload on /search pages * Avoid loading bar between photos nav. Delay loading bar by 200ms for all navigations * Update url for maps routes. Note: on page reload, next/prev is not available * Dynamically load asset-viewer on map page * When reloading a url with assetUrl, hide background page to prevent flash during load * Mostly style, review comments * Load buckets for assets on demand * Forgot this update call * typo * fix test * Fix carelessness * Review comment * merge main * remove assets * fix submodule --------- Co-authored-by: Alex <alex.tran1502@gmail.com> Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
parent
1e004611e4
commit
a78260296c
48 changed files with 289 additions and 190 deletions
|
|
@ -51,6 +51,7 @@
|
|||
import PhotoViewer from './photo-viewer.svelte';
|
||||
import SlideshowBar from './slideshow-bar.svelte';
|
||||
import VideoViewer from './video-wrapper-viewer.svelte';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
|
||||
export let assetStore: AssetStore | null = null;
|
||||
export let asset: AssetResponseDto;
|
||||
|
|
@ -191,6 +192,7 @@
|
|||
}
|
||||
|
||||
onMount(async () => {
|
||||
await navigate({ targetRoute: 'current', assetId: asset.id });
|
||||
slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
|
||||
if (value === SlideshowState.PlaySlideshow) {
|
||||
slideshowHistory.reset();
|
||||
|
|
@ -263,11 +265,14 @@
|
|||
$isShowDetail = !$isShowDetail;
|
||||
};
|
||||
|
||||
const handleCloseViewer = () => {
|
||||
closeViewer();
|
||||
const handleCloseViewer = async () => {
|
||||
await closeViewer();
|
||||
};
|
||||
|
||||
const closeViewer = () => dispatch('close');
|
||||
const closeViewer = async () => {
|
||||
dispatch('close');
|
||||
await navigate({ targetRoute: 'current', assetId: null });
|
||||
};
|
||||
|
||||
const navigateAssetRandom = async () => {
|
||||
if (!assetStore) {
|
||||
|
|
@ -300,7 +305,7 @@
|
|||
|
||||
if ($slideshowState === SlideshowState.PlaySlideshow && assetStore) {
|
||||
const hasNext =
|
||||
order === 'previous' ? await assetStore.getPreviousAsset(asset.id) : await assetStore.getNextAsset(asset.id);
|
||||
order === 'previous' ? await assetStore.getPreviousAsset(asset) : await assetStore.getNextAsset(asset);
|
||||
if (hasNext) {
|
||||
$restartSlideshowProgress = true;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
import DeleteAssetDialog from './delete-asset-dialog.svelte';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { selectAllAssets } from '$lib/utils/asset-utils';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
|
||||
export let isSelectionMode = false;
|
||||
export let singleSelect = false;
|
||||
|
|
@ -48,6 +49,10 @@
|
|||
$: isEmpty = $assetStore.initialized && $assetStore.buckets.length === 0;
|
||||
$: idsSelectedAssets = [...$selectedAssets].filter((a) => !a.isExternal).map((a) => a.id);
|
||||
|
||||
$: {
|
||||
void assetStore.updateViewport(viewport);
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>();
|
||||
|
||||
onMount(async () => {
|
||||
|
|
@ -142,22 +147,24 @@
|
|||
}
|
||||
|
||||
const handlePrevious = async () => {
|
||||
const previousAsset = await assetStore.getPreviousAsset($viewingAsset.id);
|
||||
const previousAsset = await assetStore.getPreviousAsset($viewingAsset);
|
||||
|
||||
if (previousAsset) {
|
||||
const preloadAsset = await assetStore.getPreviousAsset(previousAsset.id);
|
||||
const preloadAsset = await assetStore.getPreviousAsset(previousAsset);
|
||||
assetViewingStore.setAsset(previousAsset, preloadAsset ? [preloadAsset] : []);
|
||||
await navigate({ targetRoute: 'current', assetId: previousAsset.id });
|
||||
}
|
||||
|
||||
return !!previousAsset;
|
||||
};
|
||||
|
||||
const handleNext = async () => {
|
||||
const nextAsset = await assetStore.getNextAsset($viewingAsset.id);
|
||||
const nextAsset = await assetStore.getNextAsset($viewingAsset);
|
||||
|
||||
if (nextAsset) {
|
||||
const preloadAsset = await assetStore.getNextAsset(nextAsset.id);
|
||||
const preloadAsset = await assetStore.getNextAsset(nextAsset);
|
||||
assetViewingStore.setAsset(nextAsset, preloadAsset ? [preloadAsset] : []);
|
||||
await navigate({ targetRoute: 'current', assetId: nextAsset.id });
|
||||
}
|
||||
|
||||
return !!nextAsset;
|
||||
|
|
@ -462,8 +469,8 @@
|
|||
|
||||
<Portal target="body">
|
||||
{#if $showAssetViewer}
|
||||
{#await import('../asset-viewer/asset-viewer.svelte') then AssetViewer}
|
||||
<AssetViewer.default
|
||||
{#await import('../asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
||||
<AssetViewer
|
||||
{withStacked}
|
||||
{assetStore}
|
||||
asset={$viewingAsset}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
import { page } from '$app/stores';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { fileUploadHandler } from '$lib/utils/file-uploader';
|
||||
import { isAlbumsRoute, isSharedLinkRoute } from '$lib/utils/navigation';
|
||||
|
||||
$: albumId = ($page.route?.id === '/(user)/albums/[albumId]' || undefined) && $page.params.albumId;
|
||||
$: isShare = $page.route?.id === '/(user)/share/[key]' || undefined;
|
||||
$: albumId = isAlbumsRoute($page.route?.id) ? $page.params.albumId : undefined;
|
||||
$: isShare = isSharedLinkRoute($page.route?.id);
|
||||
|
||||
let dragStartTarget: EventTarget | null = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import Portal from '../portal/portal.svelte';
|
||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import type { BucketPosition, Viewport } from '$lib/stores/assets.store';
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
import justifiedLayout from 'justified-layout';
|
||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||
import { calculateWidth } from '$lib/utils/timeline-util';
|
||||
import { pushState, replaceState } from '$app/navigation';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
|
||||
const dispatch = createEventDispatcher<{ intersected: { container: HTMLDivElement; position: BucketPosition } }>();
|
||||
|
||||
|
|
@ -20,17 +20,15 @@
|
|||
export let showArchiveIcon = false;
|
||||
export let viewport: Viewport;
|
||||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
let { isViewing: showAssetViewer, asset: viewingAsset, setAsset } = assetViewingStore;
|
||||
|
||||
let selectedAsset: AssetResponseDto;
|
||||
let currentViewAssetIndex = 0;
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
|
||||
const viewAssetHandler = (asset: AssetResponseDto) => {
|
||||
const viewAssetHandler = async (asset: AssetResponseDto) => {
|
||||
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
|
||||
selectedAsset = assets[currentViewAssetIndex];
|
||||
$showAssetViewer = true;
|
||||
updateAssetState(selectedAsset.id, false);
|
||||
setAsset(assets[currentViewAssetIndex]);
|
||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
||||
};
|
||||
|
||||
const selectAssetHandler = (asset: AssetResponseDto) => {
|
||||
|
|
@ -45,45 +43,28 @@
|
|||
selectedAssets = temporary;
|
||||
};
|
||||
|
||||
const navigateAssetForward = () => {
|
||||
const navigateAssetForward = async () => {
|
||||
try {
|
||||
if (currentViewAssetIndex < assets.length - 1) {
|
||||
currentViewAssetIndex++;
|
||||
selectedAsset = assets[currentViewAssetIndex];
|
||||
updateAssetState(selectedAsset.id);
|
||||
setAsset(assets[++currentViewAssetIndex]);
|
||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, 'Cannot navigate to the next asset');
|
||||
}
|
||||
};
|
||||
|
||||
const navigateAssetBackward = () => {
|
||||
const navigateAssetBackward = async () => {
|
||||
try {
|
||||
if (currentViewAssetIndex > 0) {
|
||||
currentViewAssetIndex--;
|
||||
selectedAsset = assets[currentViewAssetIndex];
|
||||
updateAssetState(selectedAsset.id);
|
||||
setAsset(assets[--currentViewAssetIndex]);
|
||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, 'Cannot navigate to previous asset');
|
||||
}
|
||||
};
|
||||
|
||||
const updateAssetState = (assetId: string, replace = true) => {
|
||||
const route = `${$page.url.pathname}/photos/${assetId}`;
|
||||
|
||||
if (replace) {
|
||||
replaceState(route, {});
|
||||
} else {
|
||||
pushState(route, {});
|
||||
}
|
||||
};
|
||||
|
||||
const closeViewer = () => {
|
||||
$showAssetViewer = false;
|
||||
pushState(`${$page.url.pathname}${$page.url.search}`, {});
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
$showAssetViewer = false;
|
||||
});
|
||||
|
|
@ -107,8 +88,6 @@
|
|||
})();
|
||||
</script>
|
||||
|
||||
<svelte:window on:popstate|preventDefault={closeViewer} />
|
||||
|
||||
{#if assets.length > 0}
|
||||
<div class="relative" style="height: {geometry.containerHeight}px;width: {geometry.containerWidth}px ">
|
||||
{#each assets as asset, i (i)}
|
||||
|
|
@ -136,10 +115,7 @@
|
|||
|
||||
<!-- Overlay Asset Viewer -->
|
||||
{#if $showAssetViewer}
|
||||
<AssetViewer
|
||||
asset={selectedAsset}
|
||||
on:previous={navigateAssetBackward}
|
||||
on:next={navigateAssetForward}
|
||||
on:close={closeViewer}
|
||||
/>
|
||||
<Portal target="body">
|
||||
<AssetViewer asset={$viewingAsset} on:previous={navigateAssetBackward} on:next={navigateAssetForward} />
|
||||
</Portal>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,30 @@
|
|||
import { cubicOut } from 'svelte/easing';
|
||||
import { tweened } from 'svelte/motion';
|
||||
|
||||
let showing = false;
|
||||
|
||||
// delay showing any progress for a little bit so very fast loads
|
||||
// do not cause flicker
|
||||
const delay = 100;
|
||||
|
||||
const progress = tweened(0, {
|
||||
duration: 1000,
|
||||
easing: cubicOut,
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
await progress.set(90);
|
||||
function animate() {
|
||||
showing = true;
|
||||
void progress.set(90);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const timer = setTimeout(animate, delay);
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="absolute left-0 top-0 z-[999999999] h-[3px] w-screen bg-white">
|
||||
<span class="absolute h-[3px] bg-immich-primary" style:width={`${$progress}%`} />
|
||||
</div>
|
||||
{#if showing}
|
||||
<div class="absolute left-0 top-0 z-[999999999] h-[3px] w-screen bg-white">
|
||||
<span class="absolute h-[3px] bg-immich-primary" style:width={`${$progress}%`} />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue