tutu/src/timelines/toots/MediaAttachmentGrid.tsx

245 lines
7.3 KiB
TypeScript
Raw Normal View History

2024-07-14 20:28:44 +08:00
import type { mastodon } from "masto";
import {
type Component,
Index,
Match,
Switch,
createMemo,
createRenderEffect,
createSignal,
onCleanup,
untrack,
} from "solid-js";
2024-11-22 14:53:23 +08:00
import MediaViewer from "../MediaViewer";
import { render } from "solid-js/web";
import {
createElementSize,
useWindowSize,
} from "@solid-primitives/resize-observer";
import { useStore } from "@nanostores/solid";
2024-11-22 14:53:23 +08:00
import { $settings } from "../../settings/stores";
2024-11-22 17:16:56 +08:00
import { averageColorHex } from "~platform/blurhash";
import "./MediaAttachmentGrid.css";
2024-11-22 17:16:56 +08:00
import cardStyle from "~material/cards.module.css";
import { Preview } from "@suid/icons-material";
import { IconButton } from "@suid/material";
import Masonry from "~platform/Masonry";
2024-07-14 20:28:44 +08:00
type ElementSize = { width: number; height: number };
function constraintedSize(
{ width: owidth, height: oheight }: Readonly<ElementSize>, // originalSize
{ width: mwidth, height: mheight }: Readonly<Partial<ElementSize>>, // modifier
{ width: maxWidth, height: maxHeight }: Readonly<ElementSize>, // maxSize
) {
const ySize = owidth + (mwidth ?? 0);
const yScale = ySize > maxWidth ? ySize / maxWidth : 1;
const xSize = oheight + (mheight ?? 0);
const xScale = xSize > maxHeight ? xSize / maxHeight : 1;
const maxScale = Math.max(yScale, xScale);
const scaledWidth = owidth / maxScale;
const scaledHeight = oheight / maxScale;
return {
width: scaledWidth,
height: scaledHeight,
};
}
function isolateCallback(event: Event) {
if (event.target !== event.currentTarget) {
event.stopPropagation();
}
}
2024-07-14 20:28:44 +08:00
const MediaAttachmentGrid: Component<{
attachments: mastodon.v1.MediaAttachment[];
sensitive?: boolean;
2024-07-14 20:28:44 +08:00
}> = (props) => {
const [rootRef, setRootRef] = createSignal<HTMLElement>();
2024-07-14 20:28:44 +08:00
const [viewerIndex, setViewerIndex] = createSignal<number>();
2024-08-05 15:33:00 +08:00
const viewerOpened = () => typeof viewerIndex() !== "undefined";
const settings = useStore($settings);
const windowSize = useWindowSize();
const [reveal, setReveal] = createSignal([] as number[]);
2024-07-14 20:28:44 +08:00
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 openViewerFor = (index: number) => {
setViewerIndex(index);
2024-07-14 20:28:44 +08:00
};
const columnCount = () => {
if (props.attachments.length === 1) {
return 1;
} else if (props.attachments.length % 2 === 0) {
return 2;
} else {
return 3;
}
};
const rawElementSize = createElementSize(rootRef);
const elementWidth = () => rawElementSize.width;
const itemMaxSize = createMemo(() => {
const ewidth = elementWidth();
const width = ewidth
? (ewidth - (columnCount() - 1) * 4) / columnCount()
: 1;
return {
height: windowSize.height * 0.35,
width,
};
});
const itemStyle = (item: mastodon.v1.MediaAttachment) => {
const { width, height } = constraintedSize(
item.meta?.small || { width: 1, height: 1 },
{ width: 2, height: 2 },
itemMaxSize(),
);
const accentColor =
item.meta?.colors?.accent ??
(item.blurhash ? averageColorHex(item.blurhash) : undefined);
return Object.assign(
{
width: `${width}px`,
height: `${height}px`,
2024-10-30 13:07:55 +08:00
"contain-intrinsic-size": `${width}px ${height}px`,
},
accentColor ? { "--media-color-accent": accentColor } : {},
);
};
const isReveal = (idx: number) => {
return reveal().includes(idx);
};
const addReveal = (idx: number) => {
if (!untrack(() => isReveal(idx))) {
setReveal((x) => [...x, idx]);
}
};
2024-07-14 20:28:44 +08:00
return (
<Masonry
component="section"
ref={setRootRef}
class={`MediaAttachmentGrid ${cardStyle.cardNoPad}`}
classList={{
sensitive: props.sensitive,
}}
onClick={isolateCallback}
2024-07-14 20:28:44 +08:00
>
<Index each={props.attachments}>
2024-07-14 20:28:44 +08:00
{(item, index) => {
const itemType = () => item().type;
const style = createMemo(() => itemStyle(item()));
return (
<div style={style()} role="presentation">
<Switch>
<Match when={props.sensitive && !isReveal(index)}>
<div
class="sensitive-placeholder"
data-sort={index}
data-media-type={item().type}
>
<IconButton
color="inherit"
size="large"
onClick={[addReveal, index]}
aria-label="Reveal this media"
>
<Preview />
</IconButton>
</div>
</Match>
<Match when={itemType() === "image"}>
<img
src={item().previewUrl}
width={item().meta?.small?.width}
height={item().meta?.small?.height}
alt={item().description || undefined}
onClick={[openViewerFor, index]}
loading="lazy"
data-sort={index}
data-media-type={item().type}
></img>
</Match>
<Match when={itemType() === "video"}>
<video
src={item().url || undefined}
autoplay={!props.sensitive && settings().autoPlayVideos}
playsinline={settings().autoPlayVideos ? true : undefined}
controls
poster={item().previewUrl}
width={item().meta?.small?.width}
height={item().meta?.small?.height}
data-sort={index}
data-media-type={item().type}
preload="metadata"
/>
</Match>
<Match when={itemType() === "gifv"}>
<video
src={item().url || undefined}
autoplay={!props.sensitive && settings().autoPlayGIFs}
controls
playsinline /* or safari on iOS will play in full-screen */
loop
poster={item().previewUrl}
width={item().meta?.small?.width}
height={item().meta?.small?.height}
data-sort={index}
data-media-type={item().type}
preload="metadata"
/>
</Match>
<Match when={itemType() === "audio"}>
<audio
src={item().url || undefined}
controls
data-sort={index}
data-media-type={item().type}
></audio>
</Match>
</Switch>
</div>
);
2024-07-14 20:28:44 +08:00
}}
</Index>
</Masonry>
2024-07-14 20:28:44 +08:00
);
};
export default MediaAttachmentGrid;