mirror of
https://github.com/immich-app/immich.git
synced 2026-03-23 18:44:22 -07:00
Compare commits
17 Commits
refactor/a
...
feat/video
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86aef3ecc9 | ||
|
|
96fbc97032 | ||
|
|
d608fde175 | ||
|
|
d1e0552b9d | ||
|
|
d6cfb2b98e | ||
|
|
a733584f49 | ||
|
|
5baf860289 | ||
|
|
b67ff2c19a | ||
|
|
f45eb1e7e4 | ||
|
|
3396180d62 | ||
|
|
3359c971d4 | ||
|
|
2e17f1af16 | ||
|
|
e408cd3601 | ||
|
|
7f5ba33ab5 | ||
|
|
21b539be5d | ||
|
|
0a347d84b2 | ||
|
|
a99631b12f |
@@ -1743,6 +1743,7 @@
|
||||
"places": "Places",
|
||||
"places_count": "{count, plural, one {{count, number} Place} other {{count, number} Places}}",
|
||||
"play": "Play",
|
||||
"playback_speed": "Playback speed",
|
||||
"play_memories": "Play memories",
|
||||
"play_motion_photo": "Play Motion Photo",
|
||||
"play_or_pause_video": "Play or pause video",
|
||||
@@ -2418,6 +2419,7 @@
|
||||
"workflows": "Workflows",
|
||||
"workflows_help_text": "Workflows automate actions on your assets based on triggers and filters",
|
||||
"wrong_pin_code": "Wrong PIN code",
|
||||
"x_of_total": "{x}/{total}",
|
||||
"year": "Year",
|
||||
"years_ago": "{years, plural, one {# year} other {# years}} ago",
|
||||
"yes": "Yes",
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -815,6 +815,9 @@ importers:
|
||||
maplibre-gl:
|
||||
specifier: ^5.6.2
|
||||
version: 5.19.0
|
||||
media-chrome:
|
||||
specifier: ^4.17.2
|
||||
version: 4.17.2(react@19.2.4)
|
||||
pmtiles:
|
||||
specifier: ^4.3.0
|
||||
version: 4.4.0
|
||||
@@ -5879,6 +5882,11 @@ packages:
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
ce-la-react@0.3.2:
|
||||
resolution: {integrity: sha512-QJ6k4lOD/btI08xG8jBPxRCGXvCnusGGkTsiXk0u3NqUu/W+BXRnFD4PYjwtqh8AWmGa5LDbGk0fLQsqr0nSMA==}
|
||||
peerDependencies:
|
||||
react: '>=17.0.0'
|
||||
|
||||
chai@5.3.3:
|
||||
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -8777,6 +8785,9 @@ packages:
|
||||
mdn-data@2.0.30:
|
||||
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
||||
|
||||
media-chrome@4.17.2:
|
||||
resolution: {integrity: sha512-o/IgiHx0tdSVwRxxqF5H12FK31A/A8T71sv3KdAvh7b6XeBS9dXwqvIFwlR9kdEuqg3n7xpmRIuL83rmYq8FTg==}
|
||||
|
||||
media-typer@0.3.0:
|
||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -18138,6 +18149,10 @@ snapshots:
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
ce-la-react@0.3.2(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
|
||||
chai@5.3.3:
|
||||
dependencies:
|
||||
assertion-error: 2.0.1
|
||||
@@ -21545,6 +21560,12 @@ snapshots:
|
||||
|
||||
mdn-data@2.0.30: {}
|
||||
|
||||
media-chrome@4.17.2(react@19.2.4):
|
||||
dependencies:
|
||||
ce-la-react: 0.3.2(react@19.2.4)
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
|
||||
media-typer@0.3.0: {}
|
||||
|
||||
media-typer@1.1.0: {}
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.4.4",
|
||||
"maplibre-gl": "^5.6.2",
|
||||
"media-chrome": "^4.17.2",
|
||||
"pmtiles": "^4.3.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"simple-icons": "^15.15.0",
|
||||
|
||||
@@ -542,6 +542,7 @@
|
||||
cacheKey={asset.thumbhash}
|
||||
projectionType={asset.exifInfo?.projectionType}
|
||||
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
||||
extendedControls
|
||||
onPreviousAsset={() => navigateAsset('previous')}
|
||||
onNextAsset={() => navigateAsset('next')}
|
||||
onClose={closeViewer}
|
||||
|
||||
@@ -4,24 +4,49 @@
|
||||
import { assetViewerFadeDuration } from '$lib/constants';
|
||||
import { castManager } from '$lib/managers/cast-manager.svelte';
|
||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||
import {
|
||||
autoPlayVideo,
|
||||
loopVideo as loopVideoPreference,
|
||||
videoViewerMuted,
|
||||
videoViewerVolume,
|
||||
} from '$lib/stores/preferences.store';
|
||||
import { autoPlayVideo, loopVideo as loopVideoPreference } from '$lib/stores/preferences.store';
|
||||
import { getAssetMediaUrl, getAssetPlaybackUrl } from '$lib/utils';
|
||||
import { AssetMediaSize } from '@immich/sdk';
|
||||
import { LoadingSpinner } from '@immich/ui';
|
||||
import { timeToSeconds } from '$lib/utils/date-time';
|
||||
import { AssetMediaSize, type AssetResponseDto } from '@immich/sdk';
|
||||
import { Icon, LoadingSpinner } from '@immich/ui';
|
||||
import {
|
||||
mdiCheck,
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiFullscreen,
|
||||
mdiFullscreenExit,
|
||||
mdiPause,
|
||||
mdiPlay,
|
||||
mdiVolumeHigh,
|
||||
mdiVolumeLow,
|
||||
mdiVolumeMedium,
|
||||
mdiVolumeMute,
|
||||
} from '@mdi/js';
|
||||
import 'media-chrome/media-control-bar';
|
||||
import 'media-chrome/media-controller';
|
||||
import 'media-chrome/media-fullscreen-button';
|
||||
import 'media-chrome/media-mute-button';
|
||||
import 'media-chrome/media-play-button';
|
||||
import 'media-chrome/media-playback-rate-button';
|
||||
import 'media-chrome/media-time-display';
|
||||
import 'media-chrome/media-time-range';
|
||||
import 'media-chrome/media-volume-range';
|
||||
import 'media-chrome/menu/media-playback-rate-menu';
|
||||
import 'media-chrome/menu/media-settings-menu';
|
||||
import 'media-chrome/menu/media-settings-menu-button';
|
||||
import 'media-chrome/menu/media-settings-menu-item';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
assetId: string;
|
||||
loopVideo: boolean;
|
||||
cacheKey: string | null;
|
||||
playOriginalVideo: boolean;
|
||||
extendedControls?: boolean;
|
||||
onPreviousAsset?: () => void;
|
||||
onNextAsset?: () => void;
|
||||
onVideoEnded?: () => void;
|
||||
@@ -30,10 +55,12 @@
|
||||
}
|
||||
|
||||
let {
|
||||
asset,
|
||||
assetId,
|
||||
loopVideo,
|
||||
cacheKey,
|
||||
playOriginalVideo,
|
||||
extendedControls = false,
|
||||
onPreviousAsset = () => {},
|
||||
onNextAsset = () => {},
|
||||
onVideoEnded = () => {},
|
||||
@@ -48,12 +75,11 @@
|
||||
? getAssetMediaUrl({ id: assetId, size: AssetMediaSize.Original, cacheKey })
|
||||
: getAssetPlaybackUrl({ id: assetId, cacheKey }),
|
||||
);
|
||||
let isScrubbing = $state(false);
|
||||
let duration = $derived(timeToSeconds(asset.duration));
|
||||
let showVideo = $state(false);
|
||||
let hasFocused = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
// Show video after mount to ensure fading in.
|
||||
showVideo = true;
|
||||
});
|
||||
|
||||
@@ -73,7 +99,7 @@
|
||||
|
||||
const handleCanPlay = async (video: HTMLVideoElement) => {
|
||||
try {
|
||||
if (!video.paused && !isScrubbing) {
|
||||
if (!video.paused) {
|
||||
await video.play();
|
||||
onVideoStarted();
|
||||
}
|
||||
@@ -138,33 +164,81 @@
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<video
|
||||
bind:this={videoPlayer}
|
||||
loop={$loopVideoPreference && loopVideo}
|
||||
autoplay={$autoPlayVideo}
|
||||
playsinline
|
||||
controls
|
||||
disablePictureInPicture
|
||||
class="h-full object-contain"
|
||||
{...useSwipe(onSwipe)}
|
||||
oncanplay={(e) => handleCanPlay(e.currentTarget)}
|
||||
onended={onVideoEnded}
|
||||
onvolumechange={(e) => ($videoViewerMuted = e.currentTarget.muted)}
|
||||
onseeking={() => (isScrubbing = true)}
|
||||
onseeked={() => (isScrubbing = false)}
|
||||
onplaying={(e) => {
|
||||
if (!hasFocused) {
|
||||
e.currentTarget.focus();
|
||||
hasFocused = true;
|
||||
}
|
||||
}}
|
||||
onclose={() => onClose()}
|
||||
muted={$videoViewerMuted}
|
||||
bind:volume={$videoViewerVolume}
|
||||
poster={getAssetMediaUrl({ id: assetId, size: AssetMediaSize.Preview, cacheKey })}
|
||||
src={assetFileUrl}
|
||||
<!-- dir=ltr based on https://github.com/videojs/video.js/issues/949 -->
|
||||
<media-controller
|
||||
dir="ltr"
|
||||
nohotkeys
|
||||
class="h-full max-w-full dark"
|
||||
style={asset.width != null && asset.height != null ? `aspect-ratio: ${asset.width} / ${asset.height};` : undefined}
|
||||
defaultduration={duration}
|
||||
>
|
||||
</video>
|
||||
<video
|
||||
bind:this={videoPlayer}
|
||||
slot="media"
|
||||
loop={$loopVideoPreference && loopVideo}
|
||||
autoplay={$autoPlayVideo}
|
||||
disablePictureInPicture
|
||||
playsinline
|
||||
{...useSwipe(onSwipe)}
|
||||
class="h-full object-contain"
|
||||
oncanplay={(e) => handleCanPlay(e.currentTarget)}
|
||||
onended={onVideoEnded}
|
||||
onplaying={(e) => {
|
||||
if (!hasFocused) {
|
||||
e.currentTarget.focus();
|
||||
hasFocused = true;
|
||||
}
|
||||
}}
|
||||
onclose={onClose}
|
||||
poster={getAssetMediaUrl({ id: asset.id, size: AssetMediaSize.Preview, cacheKey })}
|
||||
src={assetFileUrl}
|
||||
></video>
|
||||
|
||||
{#if extendedControls}
|
||||
<media-settings-menu hidden anchor="auto" class="border-light-300 rounded-xl border shadow-sm w-3xs">
|
||||
<Icon slot="checked-indicator" icon={mdiCheck} class="m-2" />
|
||||
<media-settings-menu-item class="rounded-lg p-1 ps-2 mx-1">
|
||||
{$t('playback_speed')}
|
||||
<Icon slot="suffix" icon={mdiChevronRight} class="m-2" />
|
||||
<media-playback-rate-menu slot="submenu" hidden rates="0.5 1 1.5 2">
|
||||
<Icon slot="back-icon" icon={mdiChevronLeft} class="m-2" />
|
||||
<span slot="title">{$t('playback_speed')}</span>
|
||||
</media-playback-rate-menu>
|
||||
</media-settings-menu-item>
|
||||
</media-settings-menu>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col justify-end w-full h-32 px-4 bg-linear-to-b to-black/80">
|
||||
<media-control-bar part="bottom" class="flex w-full h-10 gap-2">
|
||||
<media-play-button class="rounded-full p-2 outline-none">
|
||||
<Icon slot="play" icon={mdiPlay} />
|
||||
<Icon slot="pause" icon={mdiPause} />
|
||||
</media-play-button>
|
||||
<media-time-display showduration class="rounded-lg p-2 outline-none"></media-time-display>
|
||||
|
||||
<span class="flex-grow"></span>
|
||||
|
||||
<div class="volume-wrapper rounded-full bg-light-100/0 hover:bg-light-100 transition-colors duration-400">
|
||||
<media-volume-range class="h-full bg-none outline-none"></media-volume-range>
|
||||
<media-mute-button class="p-2 bg-none outline-none">
|
||||
<Icon slot="off" icon={mdiVolumeMute} />
|
||||
<Icon slot="low" icon={mdiVolumeLow} />
|
||||
<Icon slot="medium" icon={mdiVolumeMedium} />
|
||||
<Icon slot="high" icon={mdiVolumeHigh} />
|
||||
</media-mute-button>
|
||||
</div>
|
||||
|
||||
{#if extendedControls}
|
||||
<media-fullscreen-button class="rounded-full p-2 outline-none">
|
||||
<Icon slot="enter" icon={mdiFullscreen} />
|
||||
<Icon slot="exit" icon={mdiFullscreenExit} />
|
||||
</media-fullscreen-button>
|
||||
<media-settings-menu-button class="rounded-full p-2 outline-none"></media-settings-menu-button>
|
||||
{/if}
|
||||
</media-control-bar>
|
||||
<media-time-range class="w-full h-8 px-2 pb-3 rounded-lg outline-none"></media-time-range>
|
||||
</div>
|
||||
</media-controller>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="absolute flex place-content-center place-items-center">
|
||||
@@ -178,3 +252,86 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
media-controller {
|
||||
--media-control-background: none;
|
||||
--media-control-hover-background: var(--immich-ui-light-100);
|
||||
--media-focus-box-shadow: 0 0 0 2px var(--immich-ui-dark);
|
||||
--media-font-family: var(--font-sans);
|
||||
--media-font-size: var(--text-base);
|
||||
--media-font-weight: var(--font-weight-medium);
|
||||
--media-menu-border-radius: var(--radius-xl);
|
||||
--media-menu-gap: var(--spacing);
|
||||
--media-menu-item-hover-background: var(--immich-ui-light-200);
|
||||
--media-menu-item-icon-height: 1em;
|
||||
--media-menu-item-indicator-height: 1em;
|
||||
--media-primary-color: var(--immich-ui-dark);
|
||||
--media-time-range-buffered-color: var(--immich-ui-dark-400);
|
||||
--media-time-range-hover-bottom: 0;
|
||||
--media-time-range-hover-height: 100%;
|
||||
--media-range-thumb-box-shadow: none;
|
||||
--media-range-thumb-opacity: 0;
|
||||
--media-range-thumb-transition: opacity 0.15s ease;
|
||||
--media-range-track-border-radius: 2px;
|
||||
--media-range-track-height: 3.5px;
|
||||
--media-range-padding: 0;
|
||||
--media-settings-menu-background: var(--immich-ui-light-100);
|
||||
--media-text-content-height: var(--text-base--line-height);
|
||||
--media-tooltip-arrow-display: none;
|
||||
--media-tooltip-border-radius: var(--radius-lg);
|
||||
--media-tooltip-background-color: var(--immich-ui-light-200);
|
||||
--media-tooltip-distance: 8px;
|
||||
--media-tooltip-padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3.5);
|
||||
}
|
||||
|
||||
/* Needs special handling for some reason */
|
||||
media-time-display:focus-visible {
|
||||
box-shadow: var(--media-focus-box-shadow);
|
||||
}
|
||||
|
||||
media-time-range,
|
||||
media-volume-range {
|
||||
--media-control-hover-background: none;
|
||||
}
|
||||
|
||||
media-time-range:hover,
|
||||
media-volume-range:hover {
|
||||
--media-range-thumb-opacity: 1;
|
||||
}
|
||||
|
||||
*::part(tooltip) {
|
||||
--media-font-size: var(--text-xs);
|
||||
--media-text-content-height: var(--text-xs--line-height);
|
||||
color: white;
|
||||
}
|
||||
|
||||
*[mediavolumeunavailable] {
|
||||
--media-volume-range-display: none;
|
||||
}
|
||||
|
||||
.volume-wrapper {
|
||||
--media-control-hover-background: none;
|
||||
}
|
||||
|
||||
media-volume-range:has(+ media-mute-button) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
transition: width 0.4s ease-out;
|
||||
}
|
||||
|
||||
/* Expand volume control in all relevant states */
|
||||
.volume-wrapper:hover > media-volume-range,
|
||||
media-volume-range:has(+ media-mute-button:hover),
|
||||
media-volume-range:has(+ media-mute-button:focus),
|
||||
media-volume-range:has(+ media-mute-button:focus-within),
|
||||
media-volume-range:hover,
|
||||
media-volume-range:focus,
|
||||
media-volume-range:focus-within {
|
||||
padding: 0 calc(var(--spacing) * 2);
|
||||
margin-left: calc(var(--spacing) * 2);
|
||||
width: 70px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
cacheKey: string | null;
|
||||
loopVideo: boolean;
|
||||
playOriginalVideo: boolean;
|
||||
extendedControls?: boolean;
|
||||
onClose?: () => void;
|
||||
onPreviousAsset?: () => void;
|
||||
onNextAsset?: () => void;
|
||||
@@ -25,6 +26,7 @@
|
||||
cacheKey,
|
||||
loopVideo,
|
||||
playOriginalVideo,
|
||||
extendedControls = false,
|
||||
onPreviousAsset,
|
||||
onClose,
|
||||
onNextAsset,
|
||||
@@ -41,8 +43,10 @@
|
||||
<VideoNativeViewer
|
||||
{loopVideo}
|
||||
{cacheKey}
|
||||
{asset}
|
||||
assetId={effectiveAssetId}
|
||||
{playOriginalVideo}
|
||||
{extendedControls}
|
||||
{onPreviousAsset}
|
||||
{onNextAsset}
|
||||
{onVideoEnded}
|
||||
|
||||
@@ -4,17 +4,16 @@
|
||||
import { autoPlayVideo } from '$lib/stores/preferences.store';
|
||||
import { getAssetMediaUrl, getAssetPlaybackUrl } from '$lib/utils';
|
||||
import { AssetMediaSize } from '@immich/sdk';
|
||||
import 'media-chrome/media-controller';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
asset: TimelineAsset;
|
||||
videoPlayer: HTMLVideoElement | undefined;
|
||||
videoViewerMuted?: boolean;
|
||||
videoViewerVolume?: number;
|
||||
}
|
||||
|
||||
let { asset, videoPlayer = $bindable(), videoViewerVolume, videoViewerMuted }: Props = $props();
|
||||
let { asset, videoPlayer = $bindable() }: Props = $props();
|
||||
|
||||
let showVideo: boolean = $state(false);
|
||||
|
||||
@@ -26,16 +25,19 @@
|
||||
|
||||
{#if showVideo}
|
||||
<div class="h-full w-full bg-pink-9000" transition:fade={{ duration: assetViewerFadeDuration }}>
|
||||
<video
|
||||
bind:this={videoPlayer}
|
||||
autoplay={$autoPlayVideo}
|
||||
playsinline
|
||||
class="h-full w-full rounded-2xl object-contain transition-all"
|
||||
src={getAssetPlaybackUrl({ id: asset.id })}
|
||||
poster={getAssetMediaUrl({ id: asset.id, size: AssetMediaSize.Preview })}
|
||||
draggable="false"
|
||||
muted={videoViewerMuted}
|
||||
volume={videoViewerVolume}
|
||||
></video>
|
||||
<media-controller id="memory-video" nohotkeys class="h-full w-full rounded-2xl object-contain transition-all">
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video
|
||||
bind:this={videoPlayer}
|
||||
slot="media"
|
||||
autoplay={$autoPlayVideo}
|
||||
playsinline
|
||||
disablepictureinpicture
|
||||
class="h-full w-full"
|
||||
src={getAssetPlaybackUrl({ id: asset.id })}
|
||||
poster={getAssetMediaUrl({ id: asset.id, size: AssetMediaSize.Preview })}
|
||||
draggable="false"
|
||||
></video>
|
||||
</media-controller>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -26,13 +26,13 @@
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { memoryStore, type MemoryAsset } from '$lib/stores/memory.store.svelte';
|
||||
import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import { getAssetMediaUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils';
|
||||
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
||||
import { fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { AssetMediaSize, AssetTypeEnum, getAssetInfo } from '@immich/sdk';
|
||||
import { ActionButton, IconButton, toastManager } from '@immich/ui';
|
||||
import { ActionButton, IconButton, Text, toastManager } from '@immich/ui';
|
||||
import {
|
||||
mdiCardsOutline,
|
||||
mdiChevronDown,
|
||||
@@ -52,6 +52,7 @@
|
||||
} from '@mdi/js';
|
||||
import type { NavigationTarget, Page } from '@sveltejs/kit';
|
||||
import { DateTime } from 'luxon';
|
||||
import 'media-chrome/media-mute-button';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { Attachment } from 'svelte/attachments';
|
||||
import { Tween } from 'svelte/motion';
|
||||
@@ -318,7 +319,6 @@
|
||||
|
||||
$effect(() => {
|
||||
if (videoPlayer) {
|
||||
videoPlayer.muted = $videoViewerMuted;
|
||||
initPlayer();
|
||||
}
|
||||
});
|
||||
@@ -390,42 +390,62 @@
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<div class="flex place-content-center place-items-center gap-2 overflow-hidden">
|
||||
<div class="w-12.5 dark">
|
||||
<IconButton
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
aria-label={paused ? $t('play_memories') : $t('pause_memories')}
|
||||
icon={paused ? mdiPlay : mdiPause}
|
||||
onclick={() => handlePromiseError(handleAction('PlayPauseButtonClick', paused ? 'play' : 'pause'))}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex place-content-center place-items-center gap-2 dark">
|
||||
<IconButton
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
aria-label={paused ? $t('play_memories') : $t('pause_memories')}
|
||||
icon={paused ? mdiPlay : mdiPause}
|
||||
onclick={() => handlePromiseError(handleAction('PlayPauseButtonClick', paused ? 'play' : 'pause'))}
|
||||
/>
|
||||
|
||||
{#each current.memory.assets as asset, index (asset.id)}
|
||||
<a class="relative w-full py-2" href={asHref(asset)} aria-label={$t('view')}>
|
||||
<a class="relative grow py-2" href={asHref(asset)} aria-label={$t('view')}>
|
||||
<span class="absolute start-0 h-0.5 w-full bg-gray-500"></span>
|
||||
<span class="absolute start-0 h-0.5 bg-white" style:width={`${toProgressPercentage(index)}%`}></span>
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
<div>
|
||||
<p class="text-small">
|
||||
{(current.assetIndex + 1).toLocaleString($locale)}/{current.memory.assets.length.toLocaleString($locale)}
|
||||
</p>
|
||||
</div>
|
||||
<Text size="small">
|
||||
{$t('x_of_total', {
|
||||
values: {
|
||||
x: (current.assetIndex + 1).toLocaleString($locale),
|
||||
total: current.memory.assets.length.toLocaleString($locale),
|
||||
},
|
||||
})}
|
||||
</Text>
|
||||
|
||||
{#if currentTimelineAssets.some((asset) => asset.type === AssetTypeEnum.Video)}
|
||||
<div class="w-12.5 dark">
|
||||
<media-mute-button
|
||||
mediacontroller={videoPlayer ? 'memory-video' : ''}
|
||||
disabled={!videoPlayer}
|
||||
class="bg-transparent rounded-full focus-visible:outline-2 outline-offset-2 outline-dark"
|
||||
style="--media-focus-box-shadow: none;"
|
||||
>
|
||||
<IconButton
|
||||
slot="off"
|
||||
disabled={!videoPlayer}
|
||||
tabindex={-1}
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
aria-label={$videoViewerMuted ? $t('unmute_memories') : $t('mute_memories')}
|
||||
icon={$videoViewerMuted ? mdiVolumeOff : mdiVolumeHigh}
|
||||
onclick={() => ($videoViewerMuted = !$videoViewerMuted)}
|
||||
aria-label={$t('unmute_memories')}
|
||||
icon={mdiVolumeOff}
|
||||
onclick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
slot="high"
|
||||
disabled={!videoPlayer}
|
||||
tabindex={-1}
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
aria-label={$t('mute_memories')}
|
||||
icon={mdiVolumeHigh}
|
||||
onclick={() => {}}
|
||||
/>
|
||||
</media-mute-button>
|
||||
{/if}
|
||||
</div>
|
||||
</ControlAppBar>
|
||||
@@ -497,12 +517,7 @@
|
||||
<div class="relative h-full w-full rounded-2xl bg-black">
|
||||
{#key current.asset.id}
|
||||
{#if current.asset.isVideo}
|
||||
<MemoryVideoViewer
|
||||
asset={current.asset}
|
||||
bind:videoPlayer
|
||||
videoViewerMuted={$videoViewerMuted}
|
||||
videoViewerVolume={$videoViewerVolume}
|
||||
/>
|
||||
<MemoryVideoViewer asset={current.asset} bind:videoPlayer />
|
||||
{:else}
|
||||
<MemoryPhotoViewer asset={current.asset} onImageLoad={resetAndPlay} />
|
||||
{/if}
|
||||
@@ -521,7 +536,6 @@
|
||||
color="secondary"
|
||||
aria-label={isSaved ? $t('unfavorite') : $t('favorite')}
|
||||
onclick={() => handleSaveMemory()}
|
||||
class="w-12 h-12"
|
||||
/>
|
||||
<!-- <IconButton
|
||||
icon={mdiShareVariantOutline}
|
||||
|
||||
@@ -56,9 +56,6 @@ const persistedObject = <T>(key: string, defaults: T) =>
|
||||
|
||||
export const mapSettings = persistedObject<MapSettings>('map-settings', defaultMapSettings);
|
||||
|
||||
export const videoViewerVolume = persisted<number>('video-viewer-volume', 1, {});
|
||||
export const videoViewerMuted = persisted<boolean>('video-viewer-muted', false, {});
|
||||
|
||||
export interface AlbumViewSettings {
|
||||
view: string;
|
||||
filter: string;
|
||||
|
||||
Reference in New Issue
Block a user