mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
feat(web): ascending order for slideshow (#7502)
* feat: ascending order for slideshow * feat: use dropdown * rename * fix: size * pr feedback * fix: hide text on small screen * Wording --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
db455060f0
commit
8d44afe915
@ -7,7 +7,7 @@
|
||||
import type { AssetStore } from '$lib/stores/assets.store';
|
||||
import { isShowDetail, showDeleteModal } from '$lib/stores/preferences.store';
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { getAssetJobMessage, isSharedLink, handlePromiseError } from '$lib/utils';
|
||||
@ -67,7 +67,7 @@
|
||||
const {
|
||||
restartProgress: restartSlideshowProgress,
|
||||
stopProgress: stopSlideshowProgress,
|
||||
slideshowShuffle,
|
||||
slideshowNavigation,
|
||||
slideshowState,
|
||||
} = slideshowStore;
|
||||
|
||||
@ -190,8 +190,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
shuffleSlideshowUnsubscribe = slideshowShuffle.subscribe((value) => {
|
||||
if (value) {
|
||||
shuffleSlideshowUnsubscribe = slideshowNavigation.subscribe((value) => {
|
||||
if (value === SlideshowNavigation.Shuffle) {
|
||||
slideshowHistory.reset();
|
||||
slideshowHistory.queue(asset.id);
|
||||
}
|
||||
@ -269,11 +269,11 @@
|
||||
return;
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
navigateAssetBackward();
|
||||
await navigateAsset('previous');
|
||||
return;
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
await navigateAssetForward();
|
||||
await navigateAsset('next');
|
||||
return;
|
||||
}
|
||||
case 'd':
|
||||
@ -330,13 +330,16 @@
|
||||
$restartSlideshowProgress = true;
|
||||
};
|
||||
|
||||
const navigateAssetForward = async (e?: Event) => {
|
||||
if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowShuffle) {
|
||||
return slideshowHistory.next() || navigateAssetRandom();
|
||||
const navigateAsset = async (order: 'previous' | 'next', e?: Event) => {
|
||||
if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) {
|
||||
return (order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next()) || navigateAssetRandom();
|
||||
}
|
||||
|
||||
if ($slideshowState === SlideshowState.PlaySlideshow && assetStore) {
|
||||
const hasNext = await assetStore.getNextAssetId(asset.id);
|
||||
const hasNext =
|
||||
order === 'previous'
|
||||
? await assetStore.getPreviousAssetId(asset.id)
|
||||
: await assetStore.getNextAssetId(asset.id);
|
||||
if (hasNext) {
|
||||
$restartSlideshowProgress = true;
|
||||
} else {
|
||||
@ -345,21 +348,7 @@
|
||||
}
|
||||
|
||||
e?.stopPropagation();
|
||||
dispatch('next');
|
||||
};
|
||||
|
||||
const navigateAssetBackward = (e?: Event) => {
|
||||
if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowShuffle) {
|
||||
slideshowHistory.previous();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($slideshowState === SlideshowState.PlaySlideshow) {
|
||||
$restartSlideshowProgress = true;
|
||||
}
|
||||
|
||||
e?.stopPropagation();
|
||||
dispatch('previous');
|
||||
dispatch(order);
|
||||
};
|
||||
|
||||
const showDetailInfoHandler = () => {
|
||||
@ -504,7 +493,7 @@
|
||||
|
||||
const handleVideoEnded = async () => {
|
||||
if ($slideshowState === SlideshowState.PlaySlideshow) {
|
||||
await navigateAssetForward();
|
||||
await navigateAsset('next');
|
||||
}
|
||||
};
|
||||
|
||||
@ -594,7 +583,9 @@
|
||||
|
||||
{#if $slideshowState === SlideshowState.None && showNavigation}
|
||||
<div class="z-[1001] column-span-1 col-start-1 row-span-1 row-start-2 mb-[60px] justify-self-start">
|
||||
<NavigationArea on:click={navigateAssetBackward}><Icon path={mdiChevronLeft} size="36" /></NavigationArea>
|
||||
<NavigationArea on:click={(e) => navigateAsset('previous', e)}
|
||||
><Icon path={mdiChevronLeft} size="36" /></NavigationArea
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@ -603,9 +594,9 @@
|
||||
{#if $slideshowState != SlideshowState.None}
|
||||
<div class="z-[1000] absolute w-full flex">
|
||||
<SlideshowBar
|
||||
on:prev={navigateAssetBackward}
|
||||
on:next={navigateAssetForward}
|
||||
on:close={() => ($slideshowState = SlideshowState.StopSlideshow)}
|
||||
onPrevious={() => navigateAsset('previous')}
|
||||
onNext={() => navigateAsset('next')}
|
||||
onClose={() => ($slideshowState = SlideshowState.StopSlideshow)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@ -708,7 +699,9 @@
|
||||
|
||||
{#if $slideshowState === SlideshowState.None && showNavigation}
|
||||
<div class="z-[1001] col-span-1 col-start-4 row-span-1 row-start-2 mb-[60px] justify-self-end">
|
||||
<NavigationArea on:click={navigateAssetForward}><Icon path={mdiChevronRight} size="36" /></NavigationArea>
|
||||
<NavigationArea on:click={(e) => navigateAsset('next', e)}
|
||||
><Icon path={mdiChevronRight} size="36" /></NavigationArea
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -2,11 +2,15 @@
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import ProgressBar, { ProgressBarStatus } from '$lib/components/shared-components/progress-bar/progress-bar.svelte';
|
||||
import SlideshowSettings from '$lib/components/slideshow-settings.svelte';
|
||||
import { slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { SlideshowNavigation, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiPause, mdiPlay } from '@mdi/js';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
const { restartProgress, stopProgress, slideshowDelay, showProgressBar } = slideshowStore;
|
||||
export let onNext = () => {};
|
||||
export let onPrevious = () => {};
|
||||
export let onClose = () => {};
|
||||
|
||||
const { restartProgress, stopProgress, slideshowDelay, showProgressBar, slideshowNavigation } = slideshowStore;
|
||||
|
||||
let progressBarStatus: ProgressBarStatus;
|
||||
let progressBar: ProgressBar;
|
||||
@ -15,12 +19,6 @@
|
||||
let unsubscribeRestart: () => void;
|
||||
let unsubscribeStop: () => void;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
next: void;
|
||||
prev: void;
|
||||
close: void;
|
||||
}>();
|
||||
|
||||
onMount(() => {
|
||||
unsubscribeRestart = restartProgress.subscribe((value) => {
|
||||
if (value) {
|
||||
@ -44,18 +42,26 @@
|
||||
unsubscribeStop();
|
||||
}
|
||||
});
|
||||
|
||||
const handleDone = () => {
|
||||
if ($slideshowNavigation === SlideshowNavigation.AscendingOrder) {
|
||||
onPrevious();
|
||||
return;
|
||||
}
|
||||
onNext();
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="m-4 flex gap-2">
|
||||
<CircleIconButton buttonSize="50" icon={mdiClose} on:click={() => dispatch('close')} title="Exit Slideshow" />
|
||||
<CircleIconButton buttonSize="50" icon={mdiClose} on:click={onClose} title="Exit Slideshow" />
|
||||
<CircleIconButton
|
||||
buttonSize="50"
|
||||
icon={progressBarStatus === ProgressBarStatus.Paused ? mdiPlay : mdiPause}
|
||||
on:click={() => (progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())}
|
||||
title={progressBarStatus === ProgressBarStatus.Paused ? 'Play' : 'Pause'}
|
||||
/>
|
||||
<CircleIconButton buttonSize="50" icon={mdiChevronLeft} on:click={() => dispatch('prev')} title="Previous" />
|
||||
<CircleIconButton buttonSize="50" icon={mdiChevronRight} on:click={() => dispatch('next')} title="Next" />
|
||||
<CircleIconButton buttonSize="50" icon={mdiChevronLeft} on:click={onPrevious} title="Previous" />
|
||||
<CircleIconButton buttonSize="50" icon={mdiChevronRight} on:click={onNext} title="Next" />
|
||||
<CircleIconButton buttonSize="50" icon={mdiCog} on:click={() => (showSettings = !showSettings)} title="Next" />
|
||||
</div>
|
||||
|
||||
@ -69,5 +75,5 @@
|
||||
duration={$slideshowDelay}
|
||||
bind:this={progressBar}
|
||||
bind:status={progressBarStatus}
|
||||
on:done={() => dispatch('next')}
|
||||
on:done={handleDone}
|
||||
/>
|
||||
|
@ -2,6 +2,11 @@
|
||||
// Necessary for eslint
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
type T = any;
|
||||
|
||||
export type RenderedOption = {
|
||||
title: string;
|
||||
icon?: string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" generics="T">
|
||||
@ -25,16 +30,11 @@
|
||||
|
||||
export let options: T[];
|
||||
export let selectedOption = options[0];
|
||||
|
||||
export let render: (item: T) => string | RenderedOption = String;
|
||||
|
||||
type RenderedOption = {
|
||||
title: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
export let showMenu = false;
|
||||
export let controlable = false;
|
||||
export let hideTextOnSmallScreen = true;
|
||||
|
||||
export let render: (item: T) => string | RenderedOption = String;
|
||||
|
||||
const handleClickOutside = () => {
|
||||
if (!controlable) {
|
||||
@ -76,14 +76,14 @@
|
||||
{#if renderedSelectedOption?.icon}
|
||||
<Icon path={renderedSelectedOption.icon} size="18" />
|
||||
{/if}
|
||||
<p class="hidden sm:block">{renderedSelectedOption.title}</p>
|
||||
<p class={hideTextOnSmallScreen ? 'hidden sm:block' : ''}>{renderedSelectedOption.title}</p>
|
||||
</div>
|
||||
</LinkButton>
|
||||
|
||||
<!-- DROP DOWN MENU -->
|
||||
{#if showMenu}
|
||||
<div
|
||||
transition:fly={{ y: -30, x: 30, duration: 100 }}
|
||||
transition:fly={{ y: -30, duration: 250 }}
|
||||
class="text-md fixed z-50 flex min-w-[250px] max-h-[70vh] overflow-y-auto immich-scrollbar flex-col rounded-2xl bg-gray-100 py-2 text-black shadow-lg dark:bg-gray-700 dark:text-white {className}"
|
||||
>
|
||||
{#each options as option (option)}
|
||||
|
@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Dropdown, { type RenderedOption } from '$lib/components/elements/dropdown.svelte';
|
||||
|
||||
export let title: string;
|
||||
export let subtitle = '';
|
||||
export let options: RenderedOption[];
|
||||
export let selectedOption: RenderedOption;
|
||||
export let isEdited = false;
|
||||
|
||||
export let onToggle: (option: RenderedOption) => void;
|
||||
</script>
|
||||
|
||||
<div class="flex place-items-center justify-between">
|
||||
<div>
|
||||
<div class="flex h-[26px] place-items-center gap-1">
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for={title}>
|
||||
{title}
|
||||
</label>
|
||||
{#if isEdited}
|
||||
<div
|
||||
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
|
||||
class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900"
|
||||
>
|
||||
Unsaved change
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="w-fit">
|
||||
<Dropdown
|
||||
{options}
|
||||
hideTextOnSmallScreen={false}
|
||||
bind:selectedOption
|
||||
render={(option) => {
|
||||
return {
|
||||
title: option.title,
|
||||
icon: option.icon,
|
||||
};
|
||||
}}
|
||||
on:select={({ detail }) => onToggle(detail)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
@ -4,28 +4,51 @@
|
||||
SettingInputFieldType,
|
||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { slideshowStore } from '../stores/slideshow.store';
|
||||
import { mdiArrowDownThin, mdiArrowUpThin, mdiShuffle } from '@mdi/js';
|
||||
import { SlideshowNavigation, slideshowStore } from '../stores/slideshow.store';
|
||||
import Button from './elements/buttons/button.svelte';
|
||||
import type { RenderedOption } from './elements/dropdown.svelte';
|
||||
import SettingDropdown from './shared-components/settings/setting-dropdown.svelte';
|
||||
|
||||
const { slideshowShuffle, slideshowDelay, showProgressBar } = slideshowStore;
|
||||
const { slideshowDelay, showProgressBar, slideshowNavigation } = slideshowStore;
|
||||
|
||||
export let onClose = () => {};
|
||||
|
||||
const options: Record<SlideshowNavigation, RenderedOption> = {
|
||||
[SlideshowNavigation.Shuffle]: { icon: mdiShuffle, title: 'Shuffle' },
|
||||
[SlideshowNavigation.AscendingOrder]: { icon: mdiArrowUpThin, title: 'Backward' },
|
||||
[SlideshowNavigation.DescendingOrder]: { icon: mdiArrowDownThin, title: 'Forward' },
|
||||
};
|
||||
|
||||
export const handleToggle = (selectedOption: RenderedOption) => {
|
||||
for (const [key, option] of Object.entries(options)) {
|
||||
if (option === selectedOption) {
|
||||
$slideshowNavigation = key as SlideshowNavigation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal on:clickOutside={onClose} on:escape={onClose}>
|
||||
<div
|
||||
class="flex w-96 max-w-lg flex-col gap-8 rounded-3xl border bg-white p-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
class="flex w-full md:w-96 max-w-lg flex-col gap-8 rounded-3xl border bg-white p-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
>
|
||||
<h1 class="self-center text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
Slideshow Settings
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||
<SettingSwitch title="Shuffle" bind:checked={$slideshowShuffle} />
|
||||
<SettingDropdown
|
||||
title="Direction"
|
||||
options={Object.values(options)}
|
||||
selectedOption={options[$slideshowNavigation]}
|
||||
onToggle={(option) => handleToggle(option)}
|
||||
/>
|
||||
<SettingSwitch title="Show Progress Bar" bind:checked={$showProgressBar} />
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="Delay"
|
||||
label="Duration"
|
||||
desc="Number of seconds to display each image"
|
||||
min={1}
|
||||
bind:value={$slideshowDelay}
|
||||
|
@ -7,11 +7,20 @@ export enum SlideshowState {
|
||||
None = 'none',
|
||||
}
|
||||
|
||||
export enum SlideshowNavigation {
|
||||
Shuffle = 'shuffle',
|
||||
AscendingOrder = 'ascending-order',
|
||||
DescendingOrder = 'descending-order',
|
||||
}
|
||||
|
||||
function createSlideshowStore() {
|
||||
const restartState = writable<boolean>(false);
|
||||
const stopState = writable<boolean>(false);
|
||||
|
||||
const slideshowShuffle = persisted<boolean>('slideshow-shuffle', true);
|
||||
const slideshowNavigation = persisted<SlideshowNavigation>(
|
||||
'slideshow-navigation',
|
||||
SlideshowNavigation.DescendingOrder,
|
||||
);
|
||||
const slideshowState = writable<SlideshowState>(SlideshowState.None);
|
||||
|
||||
const showProgressBar = persisted<boolean>('slideshow-show-progressbar', true);
|
||||
@ -40,7 +49,7 @@ function createSlideshowStore() {
|
||||
}
|
||||
},
|
||||
},
|
||||
slideshowShuffle,
|
||||
slideshowNavigation,
|
||||
slideshowState,
|
||||
slideshowDelay,
|
||||
showProgressBar,
|
||||
|
@ -37,7 +37,7 @@
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { AssetStore } from '$lib/stores/assets.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { downloadArchive } from '$lib/utils/asset-utils';
|
||||
import { clickOutside } from '$lib/utils/click-outside';
|
||||
@ -78,7 +78,7 @@
|
||||
export let data: PageData;
|
||||
|
||||
let { isViewing: showAssetViewer, setAssetId } = assetViewingStore;
|
||||
let { slideshowState, slideshowShuffle } = slideshowStore;
|
||||
let { slideshowState, slideshowNavigation } = slideshowStore;
|
||||
|
||||
$: album = data.album;
|
||||
$: albumId = album.id;
|
||||
@ -236,7 +236,8 @@
|
||||
};
|
||||
|
||||
const handleStartSlideshow = async () => {
|
||||
const asset = $slideshowShuffle ? await assetStore.getRandomAsset() : assetStore.assets[0];
|
||||
const asset =
|
||||
$slideshowNavigation === SlideshowNavigation.Shuffle ? await assetStore.getRandomAsset() : assetStore.assets[0];
|
||||
if (asset) {
|
||||
await setAssetId(asset.id);
|
||||
$slideshowState = SlideshowState.PlaySlideshow;
|
||||
|
Loading…
Reference in New Issue
Block a user