diff --git a/bun.lockb b/bun.lockb index 0645c92..bec25b1 100755 Binary files a/bun.lockb and b/bun.lockb differ 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 ( + + ); + }, 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 ( + { + const { top, left, width, height } = + currentTarget.getBoundingClientRect(); + }} + > + ); +} + +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 = (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 ( + { + root = e; + onMount(() => { + e.showModal(); + }); + }} + class="MediaQuickview" + classList={{ lightout: lightOut() }} + onClose={props.onClose} + onCancel={props.onClose} + onClick={onDialogClick} + > + + root.close()}> + + + + } + > + { + onMount(() => { + e.children.item(props.defaultIndex)!.scrollIntoView({ + behavior: "instant", + inline: "center", + }); + }); + }} + class="pages" + > + + {(item, index) => { + return ( + + + + + + + + ); + }} + + + + + ); +}; + +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(); - const [viewerIndex, setViewerIndex] = createSignal(); - 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 ( - 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 = () => {