mirror of
https://github.com/immich-app/immich.git
synced 2025-04-21 15:36:26 +02:00
feat(web): configure slideshow (#7219)
* feat: configure slideshow delay * feat: show/hide progressbar * fix: slider * refactor: use grid instead of flex * fix: default delay * refactor: progress bar props * refactor: slideshow settings * fix: enforce min/max value * chore: linting --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
6bb30291de
commit
b3b6426695
10 changed files with 183 additions and 119 deletions
web/src/lib
components
admin-page/settings/machine-learning-settings
asset-viewer
elements
photos-page
shared-components
progress-bar
settings
stores
|
@ -112,8 +112,8 @@
|
|||
desc="Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives."
|
||||
bind:value={config.machineLearning.facialRecognition.minScore}
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="1"
|
||||
min={0}
|
||||
max={1}
|
||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
|
||||
isEdited={config.machineLearning.facialRecognition.minScore !==
|
||||
savedConfig.machineLearning.facialRecognition.minScore}
|
||||
|
@ -125,8 +125,8 @@
|
|||
desc="Maximum distance between two faces to be considered the same person, ranging from 0-2. Lowering this can prevent labeling two people as the same person, while raising it can prevent labeling the same person as two different people. Note that it is easier to merge two people than to split one person in two, so err on the side of a lower threshold when possible."
|
||||
bind:value={config.machineLearning.facialRecognition.maxDistance}
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="2"
|
||||
min={0}
|
||||
max={2}
|
||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
|
||||
isEdited={config.machineLearning.facialRecognition.maxDistance !==
|
||||
savedConfig.machineLearning.facialRecognition.maxDistance}
|
||||
|
@ -138,7 +138,7 @@
|
|||
desc="The minimum number of recognized faces for a person to be created. Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person."
|
||||
bind:value={config.machineLearning.facialRecognition.minFaces}
|
||||
step="1"
|
||||
min="1"
|
||||
min={1}
|
||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
|
||||
isEdited={config.machineLearning.facialRecognition.minFaces !==
|
||||
savedConfig.machineLearning.facialRecognition.minFaces}
|
||||
|
|
|
@ -1,23 +1,16 @@
|
|||
<script lang="ts">
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import ProgressBar, { ProgressBarStatus } from '../shared-components/progress-bar/progress-bar.svelte';
|
||||
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 { mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiPause, mdiPlay } from '@mdi/js';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import {
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiClose,
|
||||
mdiPause,
|
||||
mdiPlay,
|
||||
mdiShuffle,
|
||||
mdiShuffleDisabled,
|
||||
} from '@mdi/js';
|
||||
|
||||
const { slideshowShuffle } = slideshowStore;
|
||||
const { restartProgress, stopProgress } = slideshowStore;
|
||||
const { restartProgress, stopProgress, slideshowDelay, showProgressBar } = slideshowStore;
|
||||
|
||||
let progressBarStatus: ProgressBarStatus;
|
||||
let progressBar: ProgressBar;
|
||||
let showSettings = false;
|
||||
|
||||
let unsubscribeRestart: () => void;
|
||||
let unsubscribeStop: () => void;
|
||||
|
@ -54,25 +47,27 @@
|
|||
</script>
|
||||
|
||||
<div class="m-4 flex gap-2">
|
||||
<CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} title="Exit Slideshow" />
|
||||
{#if $slideshowShuffle}
|
||||
<CircleIconButton icon={mdiShuffle} on:click={() => ($slideshowShuffle = false)} title="Shuffle" />
|
||||
{:else}
|
||||
<CircleIconButton icon={mdiShuffleDisabled} on:click={() => ($slideshowShuffle = true)} title="No shuffle" />
|
||||
{/if}
|
||||
<CircleIconButton buttonSize="50" icon={mdiClose} on:click={() => dispatch('close')} 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 icon={mdiChevronLeft} on:click={() => dispatch('prev')} title="Previous" />
|
||||
<CircleIconButton icon={mdiChevronRight} on:click={() => dispatch('next')} title="Next" />
|
||||
<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={mdiCog} on:click={() => (showSettings = !showSettings)} title="Next" />
|
||||
</div>
|
||||
|
||||
{#if showSettings}
|
||||
<SlideshowSettings onClose={() => (showSettings = false)} />
|
||||
{/if}
|
||||
|
||||
<ProgressBar
|
||||
autoplay
|
||||
hidden={!$showProgressBar}
|
||||
duration={$slideshowDelay}
|
||||
bind:this={progressBar}
|
||||
bind:status={progressBarStatus}
|
||||
on:done={() => dispatch('next')}
|
||||
duration={5000}
|
||||
/>
|
||||
|
|
72
web/src/lib/components/elements/slider.svelte
Normal file
72
web/src/lib/components/elements/slider.svelte
Normal file
|
@ -0,0 +1,72 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let checked = false;
|
||||
export let disabled = false;
|
||||
|
||||
const dispatch = createEventDispatcher<{ toggle: boolean }>();
|
||||
const onToggle = (event: Event) => dispatch('toggle', (event.target as HTMLInputElement).checked);
|
||||
</script>
|
||||
|
||||
<label class="relative inline-block h-[10px] w-[36px] flex-none">
|
||||
<input
|
||||
class="disabled::cursor-not-allowed h-0 w-0 opacity-0"
|
||||
type="checkbox"
|
||||
bind:checked
|
||||
on:click={onToggle}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
{#if disabled}
|
||||
<span class="slider slider-disabled cursor-not-allowed" />
|
||||
{:else}
|
||||
<span class="slider slider-enabled cursor-pointer" />
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<style>
|
||||
.slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: -4px;
|
||||
background-color: gray;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(18px);
|
||||
-ms-transform: translateX(18px);
|
||||
transform: translateX(18px);
|
||||
background-color: #4250af;
|
||||
}
|
||||
|
||||
input:checked + .slider-disabled {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
input:checked + .slider-enabled {
|
||||
background-color: #adcbfa;
|
||||
}
|
||||
</style>
|
|
@ -456,9 +456,9 @@
|
|||
asset={$viewingAsset}
|
||||
{isShared}
|
||||
{album}
|
||||
on:previous={() => handlePrevious()}
|
||||
on:next={() => handleNext()}
|
||||
on:close={() => handleClose()}
|
||||
on:previous={handlePrevious}
|
||||
on:next={handleNext}
|
||||
on:close={handleClose}
|
||||
on:action={({ detail: action }) => handleAction(action.type, action.asset)}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -15,20 +15,29 @@
|
|||
*/
|
||||
export let autoplay = false;
|
||||
|
||||
/**
|
||||
* Duration in milliseconds
|
||||
* @default 5000
|
||||
*/
|
||||
export let duration = 5000;
|
||||
|
||||
/**
|
||||
* Progress bar status
|
||||
*/
|
||||
export let status: ProgressBarStatus = ProgressBarStatus.Paused;
|
||||
|
||||
let progress = tweened<number>(0, {
|
||||
duration: (from: number, to: number) => (to ? duration * (to - from) : 0),
|
||||
});
|
||||
export let hidden = false;
|
||||
|
||||
export let duration = 5;
|
||||
|
||||
const onChange = () => {
|
||||
progress = setDuration(duration);
|
||||
play();
|
||||
};
|
||||
|
||||
let progress = setDuration(duration);
|
||||
|
||||
$: duration, onChange();
|
||||
|
||||
$: {
|
||||
if ($progress === 1) {
|
||||
dispatch('done');
|
||||
}
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
done: void;
|
||||
|
@ -67,17 +76,13 @@
|
|||
progress.set(0);
|
||||
};
|
||||
|
||||
export const setDuration = (newDuration: number) => {
|
||||
progress = tweened<number>(0, {
|
||||
duration: (from: number, to: number) => (to ? newDuration * (to - from) : 0),
|
||||
function setDuration(newDuration: number) {
|
||||
return tweened<number>(0, {
|
||||
duration: (from: number, to: number) => (to ? newDuration * 1000 * (to - from) : 0),
|
||||
});
|
||||
};
|
||||
|
||||
progress.subscribe((value) => {
|
||||
if (value === 1) {
|
||||
dispatch('done');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<span class="absolute left-0 h-[3px] bg-immich-primary shadow-2xl" style:width={`${$progress * 100}%`} />
|
||||
{#if !hidden}
|
||||
<span class="absolute left-0 h-[3px] bg-immich-primary shadow-2xl" style:width={`${$progress * 100}%`} />
|
||||
{/if}
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
|
||||
export let inputType: SettingInputFieldType;
|
||||
export let value: string | number;
|
||||
export let min = Number.MIN_SAFE_INTEGER.toString();
|
||||
export let max = Number.MAX_SAFE_INTEGER.toString();
|
||||
export let min = Number.MIN_SAFE_INTEGER;
|
||||
export let max = Number.MAX_SAFE_INTEGER;
|
||||
export let step = '1';
|
||||
export let label = '';
|
||||
export let desc = '';
|
||||
|
@ -25,15 +25,23 @@
|
|||
|
||||
const handleInput = (e: Event) => {
|
||||
value = (e.target as HTMLInputElement).value;
|
||||
|
||||
if (inputType === SettingInputFieldType.NUMBER) {
|
||||
value = Number(value) || 0;
|
||||
let newValue = Number(value) || 0;
|
||||
if (newValue < min) {
|
||||
newValue = min;
|
||||
}
|
||||
if (newValue > max) {
|
||||
newValue = max;
|
||||
}
|
||||
value = newValue;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="mb-4 w-full">
|
||||
<div class={`flex h-[26px] place-items-center gap-1`}>
|
||||
<label class={`immich-form-label text-sm`} for={label}>{label}</label>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for={label}>{label}</label>
|
||||
{#if required}
|
||||
<div class="text-red-400">*</div>
|
||||
{/if}
|
||||
|
@ -63,8 +71,8 @@
|
|||
id={label}
|
||||
name={label}
|
||||
type={inputType}
|
||||
{min}
|
||||
{max}
|
||||
min={min.toString()}
|
||||
max={max.toString()}
|
||||
{step}
|
||||
{required}
|
||||
{value}
|
||||
|
|
|
@ -25,7 +25,9 @@
|
|||
|
||||
<div class="mb-4 w-full">
|
||||
<div class={`flex h-[26px] place-items-center gap-1`}>
|
||||
<label class={`immich-form-label text-sm`} for="{name}-select">{label}</label>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="{name}-select"
|
||||
>{label}</label
|
||||
>
|
||||
|
||||
{#if isEdited}
|
||||
<div
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Slider from '$lib/components/elements/slider.svelte';
|
||||
|
||||
export let title: string;
|
||||
export let subtitle = '';
|
||||
|
@ -33,66 +34,5 @@
|
|||
<slot />
|
||||
</div>
|
||||
|
||||
<label class="relative inline-block h-[10px] w-[36px] flex-none">
|
||||
<input
|
||||
class="disabled::cursor-not-allowed h-0 w-0 opacity-0"
|
||||
type="checkbox"
|
||||
bind:checked
|
||||
on:click={onToggle}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
{#if disabled}
|
||||
<span class="slider slider-disabled cursor-not-allowed" />
|
||||
{:else}
|
||||
<span class="slider slider-enabled cursor-pointer" />
|
||||
{/if}
|
||||
</label>
|
||||
<Slider bind:checked {disabled} on:click={onToggle} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: -4px;
|
||||
background-color: gray;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(18px);
|
||||
-ms-transform: translateX(18px);
|
||||
transform: translateX(18px);
|
||||
background-color: #4250af;
|
||||
}
|
||||
|
||||
input:checked + .slider-disabled {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
input:checked + .slider-enabled {
|
||||
background-color: #adcbfa;
|
||||
}
|
||||
</style>
|
||||
|
|
37
web/src/lib/components/slideshow-settings.svelte
Normal file
37
web/src/lib/components/slideshow-settings.svelte
Normal file
|
@ -0,0 +1,37 @@
|
|||
<script lang="ts">
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import SettingInputField, {
|
||||
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 Button from './elements/buttons/button.svelte';
|
||||
|
||||
const { slideshowShuffle, slideshowDelay, showProgressBar } = slideshowStore;
|
||||
|
||||
export let onClose = () => {};
|
||||
</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"
|
||||
>
|
||||
<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} />
|
||||
<SettingSwitch title="Show Progress Bar" bind:checked={$showProgressBar} />
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="Delay"
|
||||
desc="Number of seconds to display each image"
|
||||
min={1}
|
||||
bind:value={$slideshowDelay}
|
||||
/>
|
||||
|
||||
<Button class="w-full" color="gray" on:click={onClose}>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FullScreenModal>
|
|
@ -14,6 +14,9 @@ function createSlideshowStore() {
|
|||
const slideshowShuffle = persisted<boolean>('slideshow-shuffle', true);
|
||||
const slideshowState = writable<SlideshowState>(SlideshowState.None);
|
||||
|
||||
const showProgressBar = persisted<boolean>('slideshow-show-progressbar', true);
|
||||
const slideshowDelay = persisted<number>('slideshow-delay', 5, {});
|
||||
|
||||
return {
|
||||
restartProgress: {
|
||||
subscribe: restartState.subscribe,
|
||||
|
@ -39,6 +42,8 @@ function createSlideshowStore() {
|
|||
},
|
||||
slideshowShuffle,
|
||||
slideshowState,
|
||||
slideshowDelay,
|
||||
showProgressBar,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue