1
0
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:
martin 2024-03-02 16:50:02 +01:00 committed by GitHub
parent db455060f0
commit 8d44afe915
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 144 additions and 64 deletions

View File

@ -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}

View File

@ -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}
/>

View File

@ -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)}

View File

@ -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>

View File

@ -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}

View File

@ -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,

View File

@ -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;