From 757d4e5eee056f3082ba9d91882a57c5583fe485 Mon Sep 17 00:00:00 2001 From: thislight <l1589002388@gmail.com> Date: Wed, 4 Dec 2024 20:02:29 +0800 Subject: [PATCH 1/7] first prototype of MediaQuickview --- bun.lockb | Bin 301673 -> 301705 bytes package.json | 1 + src/platform/MediaQuickview.css | 69 ++++++++ src/platform/MediaQuickview.tsx | 179 ++++++++++++++++++++ src/timelines/toots/MediaAttachmentGrid.tsx | 43 ++--- 5 files changed, 261 insertions(+), 31 deletions(-) create mode 100644 src/platform/MediaQuickview.css create mode 100644 src/platform/MediaQuickview.tsx diff --git a/bun.lockb b/bun.lockb index 0645c92b80358a366bc2a50d54c803ad3054f6c8..bec25b12c78c96f732f0b82a831c37f2ac0aa4af 100755 GIT binary patch delta 23277 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 AA^-pY delta 23020 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 diff --git a/package.json b/package.json index 219b2e0..dd31430 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@solid-primitives/map": "^0.4.13", "@solid-primitives/page-visibility": "^2.0.17", "@solid-primitives/resize-observer": "^2.0.26", + "@solid-primitives/rootless": "^1.4.5", "@solidjs/router": "^0.15.1", "@suid/icons-material": "^0.8.1", "@suid/material": "^0.18.0", diff --git a/src/platform/MediaQuickview.css b/src/platform/MediaQuickview.css new file mode 100644 index 0000000..b1ceb07 --- /dev/null +++ b/src/platform/MediaQuickview.css @@ -0,0 +1,69 @@ +.MediaQuickview__root { + display: contents; +} + +.MediaQuickview { + border: none; + position: fixed; + width: 100vw; + width: 100dvw; + height: 100vh; + height: 100dvh; + max-width: 100vw; + max-height: 100vh; + contain: content; + padding: 0; + + &::backdrop { + background: none; + } + + >.Scaffold>.topbar { + position: fixed; + left: 0; + right: 0; + + >* { + background-color: var(--tutu-color-surface); + color: var(--tutu-color-on-surface); + } + } + + >.Scaffold>.pages { + display: grid; + grid-auto-flow: column; + grid-auto-columns: 100%; + height: 100%; + width: 100%; + overflow: auto hidden; + scroll-snap-type: x mandatory; + scroll-snap-align: center; + scroll-snap-stop: always; + + + >.page { + width: 100%; + height: 100%; + max-width: 100vw; + max-height: 100vh; + contain: layout style size paint; + + >* { + display: block; + width: 100%; + height: 100%; + + object-fit: contain; + object-position: center; + } + } + } + + &.lightout { + >.Scaffold { + >.topbar { + visibility: hidden; + } + } + } +} \ No newline at end of file diff --git a/src/platform/MediaQuickview.tsx b/src/platform/MediaQuickview.tsx new file mode 100644 index 0000000..2a9d7e1 --- /dev/null +++ b/src/platform/MediaQuickview.tsx @@ -0,0 +1,179 @@ +import { createCallback, createSubRoot } from "@solid-primitives/rootless"; +import { + createRenderEffect, + createSignal, + Index, + Match, + onCleanup, + onMount, + Switch, + type Component, +} from "solid-js"; +import { render } from "solid-js/web"; +import Scaffold from "~material/Scaffold"; +import { isPointNotInRect } from "./dom"; +import "./MediaQuickview.css"; +import AppTopBar from "~material/AppTopBar"; +import { IconButton } from "@suid/material"; +import { Close } from "@suid/icons-material"; + +function renderIsolateMediaQuickview( + each: QuickviewMedia[], + index: number, + transitionFrom?: Element, +) { + createSubRoot((disposeAll) => { + let container: HTMLDivElement; + + createRenderEffect(() => { + container = document.createElement("div"); + container.setAttribute("role", "presentation"); + container.classList.add("MediaQuickview__root"); + document.querySelector("body")!.appendChild(container); + + onCleanup(() => container.remove()); + + const dispose = render(() => { + return ( + <MediaQuickview + each={each} + defaultIndex={index} + transitonFrom={transitionFrom} + onClose={disposeAll} + /> + ); + }, container); + + onCleanup(dispose); + }); + }); +} + +export function createMediaQuickview() { + return createCallback(renderIsolateMediaQuickview); +} + +function ImagePage(props: { src: string; alt?: string }) { + const [scale, setScale] = createSignal(1); + const [offsetX, setOffsetX] = createSignal(0); + const [offsetY, setOffsetY] = createSignal(0); + + const onWheel = (event: WheelEvent & { currentTarget: HTMLElement }) => { + // This is a de-facto standard for scaling: + // Browsers will simulate ctrl + wheel for two point scaling gesture. + if (event.ctrlKey) { + event.preventDefault(); + event.stopPropagation(); + + const offset = event.deltaY; + + setScale((x) => x + offset / event.currentTarget.clientHeight); + } + }; + + return ( + <img + src={props.src} + alt={props.alt} + onWheel={onWheel} + style={{ + transform: `scale(${scale()}) translateX(${offsetX()}) translateY(${offsetY()})`, + }} + onLoad={({ currentTarget }) => { + const { top, left, width, height } = + currentTarget.getBoundingClientRect(); + }} + ></img> + ); +} + +export type QuickviewMedia = { + cat: "image" | "video" | "gifv" | "audio" | "unknown"; + src: string; + alt?: string; +}; + +export type MediaQuickviewProps = { + each: QuickviewMedia[]; + defaultIndex: number; + transitonFrom?: Element; + + onClose?(): void; +}; + +const MediaQuickview: Component<MediaQuickviewProps> = (props) => { + let root: HTMLDialogElement; + const [lightOut, setLightOut] = createSignal(false); + + function onDialogClick( + event: MouseEvent & { currentTarget: HTMLDialogElement }, + ) { + event.stopPropagation(); + + if ( + isPointNotInRect( + event.currentTarget.getBoundingClientRect(), + event.clientX, + event.clientY, + ) + ) { + event.currentTarget.close(); + } else { + setLightOut((x) => !x); + } + } + + return ( + <dialog + ref={(e) => { + root = e; + onMount(() => { + e.showModal(); + }); + }} + class="MediaQuickview" + classList={{ lightout: lightOut() }} + onClose={props.onClose} + onCancel={props.onClose} + onClick={onDialogClick} + > + <Scaffold + topbar={ + <AppTopBar> + <IconButton color="inherit" onClick={(e) => root.close()}> + <Close /> + </IconButton> + </AppTopBar> + } + > + <div + ref={(e) => { + onMount(() => { + e.children.item(props.defaultIndex)!.scrollIntoView({ + behavior: "instant", + inline: "center", + }); + }); + }} + class="pages" + > + <Index each={props.each}> + {(item, index) => { + return ( + <section class="page"> + <Switch> + <Match when={item().cat === "image"}> + <ImagePage src={item().src} alt={item().alt} /> + </Match> + </Switch> + </section> + ); + }} + </Index> + </div> + </Scaffold> + </dialog> + ); +}; + +export default MediaQuickview; diff --git a/src/timelines/toots/MediaAttachmentGrid.tsx b/src/timelines/toots/MediaAttachmentGrid.tsx index c9d72fc..6291a86 100644 --- a/src/timelines/toots/MediaAttachmentGrid.tsx +++ b/src/timelines/toots/MediaAttachmentGrid.tsx @@ -5,13 +5,9 @@ import { Match, Switch, createMemo, - createRenderEffect, createSignal, - onCleanup, untrack, } from "solid-js"; -import MediaViewer from "../MediaViewer"; -import { render } from "solid-js/web"; import { createElementSize, useWindowSize, @@ -24,6 +20,7 @@ import cardStyle from "~material/cards.module.css"; import { Preview } from "@suid/icons-material"; import { IconButton } from "@suid/material"; import Masonry from "~platform/Masonry"; +import { createMediaQuickview } from "~platform/MediaQuickview"; type ElementSize = { width: number; height: number }; @@ -58,39 +55,23 @@ const MediaAttachmentGrid: Component<{ sensitive?: boolean; }> = (props) => { const [rootRef, setRootRef] = createSignal<HTMLElement>(); - const [viewerIndex, setViewerIndex] = createSignal<number>(); - const viewerOpened = () => typeof viewerIndex() !== "undefined"; const settings = useStore($settings); const windowSize = useWindowSize(); const [reveal, setReveal] = createSignal([] as number[]); - createRenderEffect(() => { - const vidx = viewerIndex(); - if (typeof vidx === "undefined") return; - const container = document.createElement("div"); - container.setAttribute("role", "presentation"); - document.body.appendChild(container); - const dispose = render(() => { - onCleanup(() => { - document.body.removeChild(container); - }); - - return ( - <MediaViewer - show={viewerOpened()} - index={viewerIndex() || 0} - onIndexUpdated={setViewerIndex} - media={props.attachments} - onClose={() => setViewerIndex()} - /> - ); - }, container); - - onCleanup(dispose); - }); + const openMediaQuickview = createMediaQuickview(); const openViewerFor = (index: number) => { - setViewerIndex(index); + openMediaQuickview( + props.attachments.map((item) => { + return { + cat: item.type, + src: item.url as string, + alt: item.description || undefined, + }; + }), + index, + ); }; const columnCount = () => { From bffa896184d8e1cf8c62366fe2383e64132d1b8c Mon Sep 17 00:00:00 2001 From: thislight <l1589002388@gmail.com> Date: Wed, 4 Dec 2024 20:45:29 +0800 Subject: [PATCH 2/7] ci: enforce type checking --- .forgejo/workflows/depoly.yml | 3 +++ package.json | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.forgejo/workflows/depoly.yml b/.forgejo/workflows/depoly.yml index edba093..0ad6d10 100644 --- a/.forgejo/workflows/depoly.yml +++ b/.forgejo/workflows/depoly.yml @@ -30,6 +30,9 @@ jobs: - name: Install Dependencies run: bun install + - name: Validate types + run: bun typecheck + - name: Build Dist (Staging) run: VITE_CODE_VERSION=$GITHUB_SHA bun dist -m staging if: env.GITHUB_REF_NAME == 'master' diff --git a/package.json b/package.json index 219b2e0..4e04928 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "dev": "vite --host 0.0.0.0", "preview": "vite preview", "dist": "vite build", - "count-source-lines": "exec scripts/src-lc.sh" + "count-source-lines": "exec scripts/src-lc.sh", + "typecheck": "tsc --noEmit --skipLibCheck" }, "keywords": [], "author": "Rubicon", From 0fca81dc93d1bcfb1c02fb00ee21c03ba9e1247d Mon Sep 17 00:00:00 2001 From: thislight <l1589002388@gmail.com> Date: Wed, 4 Dec 2024 22:57:41 +0800 Subject: [PATCH 3/7] polyfills: add Promise.withResolver --- src/platform/polyfills.ts | 21 +++++++++++++++++++++ types/lib.esnext.promise.d.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 types/lib.esnext.promise.d.ts diff --git a/src/platform/polyfills.ts b/src/platform/polyfills.ts index 032207a..1af3341 100644 --- a/src/platform/polyfills.ts +++ b/src/platform/polyfills.ts @@ -15,3 +15,24 @@ if (typeof window.crypto.randomUUID === "undefined") { ) as `${string}-${string}-${string}-${string}-${string}`; }; } + + +if (typeof Promise.withResolvers === "undefined") { + // Chrome/Edge 119, Firefox 121, Safari/iOS 17.4 + + // Promise.withResolvers is generic and works with subclasses - the typescript built-in decl + // could not handle the subclassing case. + Promise.withResolvers = function <T>(this: AnyPromiseConstructor<T>) { + let resolve!: PromiseWithResolvers<T>["resolve"], reject!: PromiseWithResolvers<T>["reject"]; + // These variables are expected to be set after `new this()` + + const promise = new this((resolve0, reject0) => { + resolve = resolve0; + reject = reject0; + }) + + return { + promise, resolve, reject + } + } +} diff --git a/types/lib.esnext.promise.d.ts b/types/lib.esnext.promise.d.ts new file mode 100644 index 0000000..5df9b56 --- /dev/null +++ b/types/lib.esnext.promise.d.ts @@ -0,0 +1,26 @@ +interface AnyPromiseWithResolvers<T, Instance> { + promise: Instance; + resolve: (value: T | PromiseLike<T>) => void; + reject: (reason?: any) => void; +} + +type AnyPromiseConstructor<T> = new ( + executor: ( + resolve: PromiseWithResolvers<T>["resolve"], + reject: PromiseWithResolvers<T>["reject"], + ) => void, +) => Promise<T>; + +interface PromiseConstructor { + /** + * Creates a new Promise and returns it in an object, along with its resolve and reject functions. + * @returns An object with the properties `promise`, `resolve`, and `reject`. + * + * ```ts + * const { promise, resolve, reject } = Promise.withResolvers<T>(); + * ``` + */ + withResolvers<T, K extends AnyPromiseConstructor<T>>( + this: K, + ): AnyPromiseWithResolvers<T, InstanceType<K>>; +} From f0456337d5b76d2a7deca73ccf547dbb2c9435f8 Mon Sep 17 00:00:00 2001 From: thislight <l1589002388@gmail.com> Date: Wed, 4 Dec 2024 23:19:04 +0800 Subject: [PATCH 4/7] ci: add checkpr --- .forgejo/workflows/checkpr.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .forgejo/workflows/checkpr.yml diff --git a/.forgejo/workflows/checkpr.yml b/.forgejo/workflows/checkpr.yml new file mode 100644 index 0000000..bb88906 --- /dev/null +++ b/.forgejo/workflows/checkpr.yml @@ -0,0 +1,34 @@ + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + depoly: + runs-on: fedora-41 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version-file: 'package.json' + + - name: Cache Dependencies + id: dependencies-cache + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install Dependencies + run: bun install + + - name: Validate types + run: bun typecheck + + - name: Build Dist + run: VITE_CODE_VERSION=$GITHUB_SHA bun dist From f20b5da8e4c7c340c156e5fd788fcda6b789dd6c Mon Sep 17 00:00:00 2001 From: thislight <l1589002388@gmail.com> Date: Wed, 4 Dec 2024 23:42:16 +0800 Subject: [PATCH 5/7] serviceworker: fix type errors --- src/serviceworker/main.ts | 31 ++++++++++------- src/serviceworker/tsconfig.json | 6 ++-- src/serviceworker/workerrpc.ts | 61 +++++++++++++++++++-------------- tsconfig.json | 19 +++------- tsconfig.super.json | 17 +++++++++ 5 files changed, 80 insertions(+), 54 deletions(-) create mode 100644 tsconfig.super.json diff --git a/src/serviceworker/main.ts b/src/serviceworker/main.ts index 71a4f37..99305a9 100644 --- a/src/serviceworker/main.ts +++ b/src/serviceworker/main.ts @@ -4,23 +4,27 @@ import { dispatchCall, isJSONRPCCall, type Call } from "./workerrpc"; function isServiceWorker( self: WorkerGlobalScope, + // @ts-ignore: workaround for workbox logger.d.ts decl ): self is ServiceWorkerGlobalScope { - return !!(self as unknown as ServiceWorkerGlobalScope).registration; + return ( + (self as unknown as Record<string, unknown>)["serviceWorker"] instanceof + ServiceWorker + ); } -if (isServiceWorker(self)) { - cleanupOutdatedCaches(); - precacheAndRoute(self.__WB_MANIFEST, { - cleanURLs: false, - }); - - // auto update - self.skipWaiting(); - clientsClaim(); -} else { +if (!isServiceWorker(self)) { throw new TypeError("This entry point must be run in a service worker"); } +cleanupOutdatedCaches(); +precacheAndRoute(self.__WB_MANIFEST, { + cleanURLs: false, +}); + +// auto update +self.skipWaiting(); +clientsClaim(); + export const Service = { ping() {}, }; @@ -29,6 +33,9 @@ self.addEventListener("message", (event: MessageEvent<unknown>) => { const payload = event.data; if (typeof payload !== "object") return; if (isJSONRPCCall(payload as Record<string, unknown>)) { - dispatchCall(Service, event as MessageEvent<Call<unknown>>); + dispatchCall( + Service, + event as MessageEvent<Call<unknown>> & { source: MessageEventSource }, + ); } }); diff --git a/src/serviceworker/tsconfig.json b/src/serviceworker/tsconfig.json index bc88f57..090edc2 100644 --- a/src/serviceworker/tsconfig.json +++ b/src/serviceworker/tsconfig.json @@ -1,5 +1,7 @@ { "compilerOptions": { - "lib": ["ESNext", "WebWorker"], + "lib": ["WebWorker", "ESNext"], }, -} \ No newline at end of file + "extends": ["../../tsconfig.super.json"], + "include": ["./**/*.ts"], +} diff --git a/src/serviceworker/workerrpc.ts b/src/serviceworker/workerrpc.ts index 84bb39f..7680716 100644 --- a/src/serviceworker/workerrpc.ts +++ b/src/serviceworker/workerrpc.ts @@ -46,7 +46,7 @@ export type Result<T, E> = JSONRPC & { id: string | number } & ( export function isJSONRPCResult( object: Record<string, unknown>, ): object is Result<unknown, unknown> { - return object["jsonrpc"] === "2.0" && object["id"] && !object["method"]; + return !!(object["jsonrpc"] === "2.0" && object["id"] && !object["method"]); } export function isJSONRPCCall( @@ -114,6 +114,9 @@ export class ResultDispatcher { { const callback = this.map.get(id); if (!callback) return; + // Now the callback is not undefined + + // Fast path if (typeof callback !== "boolean") { callback(message); this.map.delete(id); @@ -121,28 +124,30 @@ export class ResultDispatcher { } } - return new Promise<void>((resolve) => { - let retried = 0; + const { promise, resolve } = Promise.withResolvers<undefined>(); - const checkAndDispatch = () => { - const callback = this.map.get(id); - if (typeof callback !== "boolean") { - callback(message); - this.map.delete(id); - resolve(); - return; - } - setTimeout(checkAndDispatch, 0); - if (++retried > 3) { - console.warn( - `retried ${retried} time(s) but the callback is still disappeared, id is "${id}"`, - ); - } - }; + let retried = 0; - // start the loop - checkAndDispatch(); - }); + const checkAndDispatch = () => { + const callback = this.map.get(id); + if (typeof callback !== "boolean") { + callback!(message); // the nullability is already checked before + this.map.delete(id); + resolve(undefined); + return; + } + setTimeout(checkAndDispatch, 0); + if (++retried > 3) { + console.warn( + `retried ${retried} time(s) but the callback is still disappeared, id is "${id}"`, + ); + } + }; + + // start the loop + checkAndDispatch(); + + return promise; } createTypedCall< @@ -159,14 +164,16 @@ export class ResultDispatcher { } } -type AnyService = Record<string, ((...args: unknown[]) => unknown) | undefined> +type AnyService = Record<string, (...args: unknown[]) => unknown>; -export async function dispatchCall< - S extends AnyService, ->(service: S, event: MessageEvent<Call<unknown>>) { +export async function dispatchCall<S extends Partial<AnyService>>( + service: S, + event: MessageEvent<Call<unknown>> & { source: MessageEventSource }, +) { try { const fn = service[event.data.method]; if (!fn) { + console.warn("requested unknown method", event.data.method, event.data); if (event.data.id) return event.source.postMessage({ jsonrpc: "2.0", @@ -176,10 +183,12 @@ export async function dispatchCall< message: "Method not found", }, } as Result<void, void>); + + return; } try { - const result = await fn(...event.data.params as unknown[]); + const result = await fn(...(event.data.params as unknown[])); if (!event.data.id) return; diff --git a/tsconfig.json b/tsconfig.json index b92e528..feaab58 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,11 @@ { "compilerOptions": { - "strict": true, - "target": "ESNext", - "module": "esnext", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, "jsx": "preserve", "jsxImportSource": "solid-js", "types": ["vite/client", "vite-plugin-pwa/solid"], - "noEmit": true, - "isolatedModules": true, - "resolveJsonModule": true, - "paths": { - "~platform/*": ["./src/platform/*"], - "~material/*": ["./src/material/*"] - } - } + "lib": ["ESNext", "DOM", "DOM.Iterable"] + }, + "include": ["./src/**/*.ts", "./src/**/*.tsx", "./*.ts", "./types/**.ts"], + "exclude": ["./src/serviceworker/**"], + "extends": ["./tsconfig.super.json"] } diff --git a/tsconfig.super.json b/tsconfig.super.json new file mode 100644 index 0000000..9cf8e60 --- /dev/null +++ b/tsconfig.super.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ESNext", + "module": "esnext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "noEmit": true, + "isolatedModules": true, + "resolveJsonModule": true, + "paths": { + "~platform/*": ["./src/platform/*"], + "~material/*": ["./src/material/*"] + } + } +} From 4d4e787b84f1026417567ea29441572819cadad1 Mon Sep 17 00:00:00 2001 From: thislight <l1589002388@gmail.com> Date: Wed, 4 Dec 2024 23:45:59 +0800 Subject: [PATCH 6/7] ci/checkpr: correct the job name --- .forgejo/workflows/checkpr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/checkpr.yml b/.forgejo/workflows/checkpr.yml index bb88906..2f6dc9f 100644 --- a/.forgejo/workflows/checkpr.yml +++ b/.forgejo/workflows/checkpr.yml @@ -4,7 +4,7 @@ on: types: [opened, synchronize, reopened] jobs: - depoly: + checkpr: runs-on: fedora-41 steps: - name: Checkout From a15e2d62fe7dd5e5e7be78648a767f4e7c84a71f Mon Sep 17 00:00:00 2001 From: thislight <l1589002388@gmail.com> Date: Wed, 4 Dec 2024 20:02:29 +0800 Subject: [PATCH 7/7] first prototype of MediaQuickview --- bun.lockb | Bin 301673 -> 301705 bytes package.json | 1 + src/platform/MediaQuickview.css | 69 ++++++++ src/platform/MediaQuickview.tsx | 179 ++++++++++++++++++++ src/timelines/toots/MediaAttachmentGrid.tsx | 43 ++--- 5 files changed, 261 insertions(+), 31 deletions(-) create mode 100644 src/platform/MediaQuickview.css create mode 100644 src/platform/MediaQuickview.tsx diff --git a/bun.lockb b/bun.lockb index 0645c92b80358a366bc2a50d54c803ad3054f6c8..bec25b12c78c96f732f0b82a831c37f2ac0aa4af 100755 GIT binary patch delta 23277 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 AA^-pY delta 23020 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 diff --git a/package.json b/package.json index 4e04928..6222ecd 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@solid-primitives/map": "^0.4.13", "@solid-primitives/page-visibility": "^2.0.17", "@solid-primitives/resize-observer": "^2.0.26", + "@solid-primitives/rootless": "^1.4.5", "@solidjs/router": "^0.15.1", "@suid/icons-material": "^0.8.1", "@suid/material": "^0.18.0", diff --git a/src/platform/MediaQuickview.css b/src/platform/MediaQuickview.css new file mode 100644 index 0000000..b1ceb07 --- /dev/null +++ b/src/platform/MediaQuickview.css @@ -0,0 +1,69 @@ +.MediaQuickview__root { + display: contents; +} + +.MediaQuickview { + border: none; + position: fixed; + width: 100vw; + width: 100dvw; + height: 100vh; + height: 100dvh; + max-width: 100vw; + max-height: 100vh; + contain: content; + padding: 0; + + &::backdrop { + background: none; + } + + >.Scaffold>.topbar { + position: fixed; + left: 0; + right: 0; + + >* { + background-color: var(--tutu-color-surface); + color: var(--tutu-color-on-surface); + } + } + + >.Scaffold>.pages { + display: grid; + grid-auto-flow: column; + grid-auto-columns: 100%; + height: 100%; + width: 100%; + overflow: auto hidden; + scroll-snap-type: x mandatory; + scroll-snap-align: center; + scroll-snap-stop: always; + + + >.page { + width: 100%; + height: 100%; + max-width: 100vw; + max-height: 100vh; + contain: layout style size paint; + + >* { + display: block; + width: 100%; + height: 100%; + + object-fit: contain; + object-position: center; + } + } + } + + &.lightout { + >.Scaffold { + >.topbar { + visibility: hidden; + } + } + } +} \ No newline at end of file diff --git a/src/platform/MediaQuickview.tsx b/src/platform/MediaQuickview.tsx new file mode 100644 index 0000000..2a9d7e1 --- /dev/null +++ b/src/platform/MediaQuickview.tsx @@ -0,0 +1,179 @@ +import { createCallback, createSubRoot } from "@solid-primitives/rootless"; +import { + createRenderEffect, + createSignal, + Index, + Match, + onCleanup, + onMount, + Switch, + type Component, +} from "solid-js"; +import { render } from "solid-js/web"; +import Scaffold from "~material/Scaffold"; +import { isPointNotInRect } from "./dom"; +import "./MediaQuickview.css"; +import AppTopBar from "~material/AppTopBar"; +import { IconButton } from "@suid/material"; +import { Close } from "@suid/icons-material"; + +function renderIsolateMediaQuickview( + each: QuickviewMedia[], + index: number, + transitionFrom?: Element, +) { + createSubRoot((disposeAll) => { + let container: HTMLDivElement; + + createRenderEffect(() => { + container = document.createElement("div"); + container.setAttribute("role", "presentation"); + container.classList.add("MediaQuickview__root"); + document.querySelector("body")!.appendChild(container); + + onCleanup(() => container.remove()); + + const dispose = render(() => { + return ( + <MediaQuickview + each={each} + defaultIndex={index} + transitonFrom={transitionFrom} + onClose={disposeAll} + /> + ); + }, container); + + onCleanup(dispose); + }); + }); +} + +export function createMediaQuickview() { + return createCallback(renderIsolateMediaQuickview); +} + +function ImagePage(props: { src: string; alt?: string }) { + const [scale, setScale] = createSignal(1); + const [offsetX, setOffsetX] = createSignal(0); + const [offsetY, setOffsetY] = createSignal(0); + + const onWheel = (event: WheelEvent & { currentTarget: HTMLElement }) => { + // This is a de-facto standard for scaling: + // Browsers will simulate ctrl + wheel for two point scaling gesture. + if (event.ctrlKey) { + event.preventDefault(); + event.stopPropagation(); + + const offset = event.deltaY; + + setScale((x) => x + offset / event.currentTarget.clientHeight); + } + }; + + return ( + <img + src={props.src} + alt={props.alt} + onWheel={onWheel} + style={{ + transform: `scale(${scale()}) translateX(${offsetX()}) translateY(${offsetY()})`, + }} + onLoad={({ currentTarget }) => { + const { top, left, width, height } = + currentTarget.getBoundingClientRect(); + }} + ></img> + ); +} + +export type QuickviewMedia = { + cat: "image" | "video" | "gifv" | "audio" | "unknown"; + src: string; + alt?: string; +}; + +export type MediaQuickviewProps = { + each: QuickviewMedia[]; + defaultIndex: number; + transitonFrom?: Element; + + onClose?(): void; +}; + +const MediaQuickview: Component<MediaQuickviewProps> = (props) => { + let root: HTMLDialogElement; + const [lightOut, setLightOut] = createSignal(false); + + function onDialogClick( + event: MouseEvent & { currentTarget: HTMLDialogElement }, + ) { + event.stopPropagation(); + + if ( + isPointNotInRect( + event.currentTarget.getBoundingClientRect(), + event.clientX, + event.clientY, + ) + ) { + event.currentTarget.close(); + } else { + setLightOut((x) => !x); + } + } + + return ( + <dialog + ref={(e) => { + root = e; + onMount(() => { + e.showModal(); + }); + }} + class="MediaQuickview" + classList={{ lightout: lightOut() }} + onClose={props.onClose} + onCancel={props.onClose} + onClick={onDialogClick} + > + <Scaffold + topbar={ + <AppTopBar> + <IconButton color="inherit" onClick={(e) => root.close()}> + <Close /> + </IconButton> + </AppTopBar> + } + > + <div + ref={(e) => { + onMount(() => { + e.children.item(props.defaultIndex)!.scrollIntoView({ + behavior: "instant", + inline: "center", + }); + }); + }} + class="pages" + > + <Index each={props.each}> + {(item, index) => { + return ( + <section class="page"> + <Switch> + <Match when={item().cat === "image"}> + <ImagePage src={item().src} alt={item().alt} /> + </Match> + </Switch> + </section> + ); + }} + </Index> + </div> + </Scaffold> + </dialog> + ); +}; + +export default MediaQuickview; diff --git a/src/timelines/toots/MediaAttachmentGrid.tsx b/src/timelines/toots/MediaAttachmentGrid.tsx index c9d72fc..6291a86 100644 --- a/src/timelines/toots/MediaAttachmentGrid.tsx +++ b/src/timelines/toots/MediaAttachmentGrid.tsx @@ -5,13 +5,9 @@ import { Match, Switch, createMemo, - createRenderEffect, createSignal, - onCleanup, untrack, } from "solid-js"; -import MediaViewer from "../MediaViewer"; -import { render } from "solid-js/web"; import { createElementSize, useWindowSize, @@ -24,6 +20,7 @@ import cardStyle from "~material/cards.module.css"; import { Preview } from "@suid/icons-material"; import { IconButton } from "@suid/material"; import Masonry from "~platform/Masonry"; +import { createMediaQuickview } from "~platform/MediaQuickview"; type ElementSize = { width: number; height: number }; @@ -58,39 +55,23 @@ const MediaAttachmentGrid: Component<{ sensitive?: boolean; }> = (props) => { const [rootRef, setRootRef] = createSignal<HTMLElement>(); - const [viewerIndex, setViewerIndex] = createSignal<number>(); - const viewerOpened = () => typeof viewerIndex() !== "undefined"; const settings = useStore($settings); const windowSize = useWindowSize(); const [reveal, setReveal] = createSignal([] as number[]); - createRenderEffect(() => { - const vidx = viewerIndex(); - if (typeof vidx === "undefined") return; - const container = document.createElement("div"); - container.setAttribute("role", "presentation"); - document.body.appendChild(container); - const dispose = render(() => { - onCleanup(() => { - document.body.removeChild(container); - }); - - return ( - <MediaViewer - show={viewerOpened()} - index={viewerIndex() || 0} - onIndexUpdated={setViewerIndex} - media={props.attachments} - onClose={() => setViewerIndex()} - /> - ); - }, container); - - onCleanup(dispose); - }); + const openMediaQuickview = createMediaQuickview(); const openViewerFor = (index: number) => { - setViewerIndex(index); + openMediaQuickview( + props.attachments.map((item) => { + return { + cat: item.type, + src: item.url as string, + alt: item.description || undefined, + }; + }), + index, + ); }; const columnCount = () => {