mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
feat: mobile-web improvements - scrubber (#16856)
* feat: mobile-web improvements - scrubber * lint * cruft * lint * fix: thumb style --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
b5d5c40c69
commit
55b52ecbec
@ -5,7 +5,7 @@
|
||||
<!-- metadata:tags -->
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||
|
@ -25,6 +25,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import type { UpdatePayload } from 'vite';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
|
||||
interface Props {
|
||||
isSelectionMode?: boolean;
|
||||
@ -82,6 +83,8 @@
|
||||
let bottomSectionHeight = 60;
|
||||
let leadout = $state(false);
|
||||
|
||||
const usingMobileDevice = $derived(mobileDevice.hoverNone);
|
||||
|
||||
const scrollTo = (top: number) => {
|
||||
element?.scrollTo({ top });
|
||||
showSkeleton = false;
|
||||
@ -714,7 +717,12 @@
|
||||
<!-- Right margin MUST be equal to the width of immich-scrubbable-scrollbar -->
|
||||
<section
|
||||
id="asset-grid"
|
||||
class="scrollbar-hidden h-full overflow-y-auto outline-none {isEmpty ? 'm-0' : 'ml-4 tall:ml-0 mr-[60px]'}"
|
||||
class={[
|
||||
'scrollbar-hidden h-full overflow-y-auto outline-none',
|
||||
{ 'm-0': isEmpty },
|
||||
{ 'ml-4 tall:ml-0': !isEmpty },
|
||||
{ 'mr-[60px]': !isEmpty && !usingMobileDevice },
|
||||
]}
|
||||
tabindex="-1"
|
||||
bind:clientHeight={assetStore.viewportHeight}
|
||||
bind:clientWidth={null, (v) => ((assetStore.viewportWidth = v), updateSlidingWindow())}
|
||||
|
@ -1,8 +1,12 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import type { AssetStore, LiteBucket } from '$lib/stores/assets-store.svelte';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { fromLocalDateTime, type ScrubberListener } from '$lib/utils/timeline-util';
|
||||
import { mdiPlay } from '@mdi/js';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
@ -209,6 +213,62 @@
|
||||
|
||||
void onScrub?.(bucketDate, scrollPercent, bucketPercentY);
|
||||
};
|
||||
const getTouch = (event: TouchEvent) => {
|
||||
if (event.touches.length === 1) {
|
||||
return event.touches[0];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const onTouchStart = (event: TouchEvent) => {
|
||||
const touch = getTouch(event);
|
||||
if (!touch) {
|
||||
isHover = false;
|
||||
return;
|
||||
}
|
||||
const elements = document.elementsFromPoint(touch.clientX, touch.clientY);
|
||||
const isHoverScrollbar = elements.some(({ id }) => {
|
||||
return id === 'immich-scrubbable-scrollbar' || id === 'time-label';
|
||||
});
|
||||
|
||||
isHover = isHoverScrollbar;
|
||||
|
||||
if (isHoverScrollbar) {
|
||||
handleMouseEvent({
|
||||
clientY: touch.clientY,
|
||||
isDragging: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
const onTouchEnd = () => {
|
||||
if (isHover) {
|
||||
isHover = false;
|
||||
}
|
||||
handleMouseEvent({
|
||||
clientY,
|
||||
isDragging: false,
|
||||
});
|
||||
};
|
||||
const onTouchMove = (event: TouchEvent) => {
|
||||
const touch = getTouch(event);
|
||||
if (touch && isDragging) {
|
||||
handleMouseEvent({
|
||||
clientY: touch.clientY,
|
||||
});
|
||||
event.preventDefault();
|
||||
} else {
|
||||
isHover = false;
|
||||
}
|
||||
};
|
||||
onMount(() => {
|
||||
const opts = {
|
||||
passive: false,
|
||||
};
|
||||
globalThis.addEventListener('touchmove', onTouchMove, opts);
|
||||
return () => {
|
||||
globalThis.removeEventListener('touchmove', onTouchMove);
|
||||
};
|
||||
});
|
||||
const usingMobileDevice = $derived(mobileDevice.hoverNone);
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
@ -216,6 +276,9 @@
|
||||
onmousemove={({ clientY }) => (isDragging || isHover) && handleMouseEvent({ clientY })}
|
||||
onmousedown={({ clientY }) => isHover && handleMouseEvent({ clientY, isDragging: true })}
|
||||
onmouseup={({ clientY }) => handleMouseEvent({ clientY, isDragging: false })}
|
||||
ontouchstart={onTouchStart}
|
||||
ontouchend={onTouchEnd}
|
||||
ontouchcancel={onTouchEnd}
|
||||
/>
|
||||
|
||||
<div
|
||||
@ -237,8 +300,9 @@
|
||||
onmouseenter={() => (isHover = true)}
|
||||
onmouseleave={() => (isHover = false)}
|
||||
onkeydown={(event) => onScrubKeyDown?.(event, event.currentTarget)}
|
||||
draggable="false"
|
||||
>
|
||||
{#if hoverLabel && (isHover || isDragging)}
|
||||
{#if !usingMobileDevice && hoverLabel && (isHover || isDragging)}
|
||||
<div
|
||||
id="time-label"
|
||||
class={[
|
||||
@ -251,8 +315,34 @@
|
||||
{hoverLabel}
|
||||
</div>
|
||||
{/if}
|
||||
{#if usingMobileDevice && ((assetStore.scrolling && scrollHoverLabel) || isHover || isDragging)}
|
||||
<div
|
||||
id="time-label"
|
||||
class="rounded-l-full w-[32px] pl-2 text-white bg-immich-primary dark:bg-gray-600 hover:cursor-pointer select-none"
|
||||
style:top="{scrollY + HOVER_DATE_HEIGHT - 25}px"
|
||||
style:height="50px"
|
||||
style:right="0"
|
||||
style:position="absolute"
|
||||
in:fade={{ duration: 200 }}
|
||||
out:fade={{ duration: 200 }}
|
||||
>
|
||||
<Icon path={mdiPlay} size="20" class="-rotate-90 relative top-[9px] -right-[2px]" />
|
||||
<Icon path={mdiPlay} size="20" class="rotate-90 relative top-[1px] -right-[2px]" />
|
||||
{#if (assetStore.scrolling && scrollHoverLabel) || isHover || isDragging}
|
||||
<p
|
||||
transition:fade={{ duration: 200 }}
|
||||
style:bottom={50 / 2 - 30 / 2 + 'px'}
|
||||
style:right="36px"
|
||||
style:width="fit-content"
|
||||
class="truncate pointer-events-none absolute text-sm rounded-full w-[32px] py-2 px-4 text-white bg-immich-primary/90 dark:bg-gray-500 hover:cursor-pointer select-none font-semibold"
|
||||
>
|
||||
{scrollHoverLabel}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Scroll Position Indicator Line -->
|
||||
{#if !isDragging}
|
||||
{#if !usingMobileDevice && !isDragging}
|
||||
<div
|
||||
class="absolute right-0 h-[2px] w-10 bg-immich-primary dark:bg-immich-dark-primary"
|
||||
style:top="{scrollY + HOVER_DATE_HEIGHT}px"
|
||||
@ -280,21 +370,14 @@
|
||||
data-time-segment-bucket-date={segment.date}
|
||||
data-label={segment.dateFormatted}
|
||||
style:height={segment.height + 'px'}
|
||||
aria-label={segment.dateFormatted + ' ' + segment.count}
|
||||
>
|
||||
{#if segment.hasLabel}
|
||||
<div
|
||||
aria-label={segment.dateFormatted + ' ' + segment.count}
|
||||
class="absolute right-[1.25rem] top-[-16px] z-10 text-[12px] dark:text-immich-dark-fg font-immich-mono"
|
||||
>
|
||||
{#if !usingMobileDevice && segment.hasLabel}
|
||||
<div class="absolute right-[1.25rem] top-[-16px] z-10 text-[12px] dark:text-immich-dark-fg font-immich-mono">
|
||||
{segment.date.year}
|
||||
</div>
|
||||
{/if}
|
||||
{#if segment.hasDot}
|
||||
<div
|
||||
aria-label={segment.dateFormatted + ' ' + segment.count}
|
||||
class="absolute right-[0.75rem] bottom-0 h-[4px] w-[4px] rounded-full bg-gray-300"
|
||||
></div>
|
||||
{#if !usingMobileDevice && segment.hasDot}
|
||||
<div class="absolute right-[0.75rem] bottom-0 h-[4px] w-[4px] rounded-full bg-gray-300"></div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
9
web/src/lib/stores/mobile-device.svelte.ts
Normal file
9
web/src/lib/stores/mobile-device.svelte.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { MediaQuery } from 'svelte/reactivity';
|
||||
|
||||
const hoverNone = new MediaQuery('hover: none');
|
||||
|
||||
export const mobileDevice = {
|
||||
get hoverNone() {
|
||||
return hoverNone.current;
|
||||
},
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user