2024-07-14 20:28:44 +08:00
|
|
|
import type { mastodon } from "masto";
|
2024-09-14 20:43:44 +08:00
|
|
|
import {
|
|
|
|
type Component,
|
2024-10-29 19:35:40 +08:00
|
|
|
Index,
|
2024-11-11 14:57:49 +08:00
|
|
|
Match,
|
|
|
|
Switch,
|
2024-10-28 18:26:59 +08:00
|
|
|
createMemo,
|
2024-09-14 21:17:03 +08:00
|
|
|
createRenderEffect,
|
2024-09-14 20:43:44 +08:00
|
|
|
createSignal,
|
|
|
|
onCleanup,
|
2024-11-20 21:09:14 +08:00
|
|
|
untrack,
|
2024-09-14 20:43:44 +08:00
|
|
|
} from "solid-js";
|
2024-08-12 21:55:26 +08:00
|
|
|
import MediaViewer from "./MediaViewer";
|
2024-09-14 20:21:19 +08:00
|
|
|
import { render } from "solid-js/web";
|
2024-10-28 18:26:59 +08:00
|
|
|
import {
|
|
|
|
createElementSize,
|
|
|
|
useWindowSize,
|
|
|
|
} from "@solid-primitives/resize-observer";
|
2024-10-09 18:45:19 +08:00
|
|
|
import { useStore } from "@nanostores/solid";
|
|
|
|
import { $settings } from "../settings/stores";
|
2024-11-11 16:53:34 +08:00
|
|
|
import { averageColorHex } from "../platform/blurhash";
|
2024-11-12 19:12:49 +08:00
|
|
|
import "./MediaAttachmentGrid.css";
|
|
|
|
import cardStyle from "../material/cards.module.css";
|
2024-11-20 21:09:14 +08:00
|
|
|
import { Preview } from "@suid/icons-material";
|
|
|
|
import { IconButton } from "@suid/material";
|
2024-07-14 20:28:44 +08:00
|
|
|
|
2024-10-28 18:26:59 +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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-11-20 20:34:30 +08:00
|
|
|
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[];
|
2024-11-20 20:34:30 +08:00
|
|
|
sensitive?: boolean;
|
2024-07-14 20:28:44 +08:00
|
|
|
}> = (props) => {
|
2024-10-28 18:26:59 +08:00
|
|
|
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";
|
2024-10-09 18:45:19 +08:00
|
|
|
const settings = useStore($settings);
|
2024-10-28 18:26:59 +08:00
|
|
|
const windowSize = useWindowSize();
|
2024-11-20 21:09:14 +08:00
|
|
|
const [reveal, setReveal] = createSignal([] as number[]);
|
2024-07-14 20:28:44 +08:00
|
|
|
|
2024-11-20 20:34:30 +08:00
|
|
|
createRenderEffect(() => {
|
2024-09-14 21:17:03 +08:00
|
|
|
const vidx = viewerIndex();
|
|
|
|
if (typeof vidx === "undefined") return;
|
2024-09-14 20:21:19 +08:00
|
|
|
const container = document.createElement("div");
|
|
|
|
container.setAttribute("role", "presentation");
|
|
|
|
document.body.appendChild(container);
|
2024-11-20 20:34:30 +08:00
|
|
|
const dispose = render(() => {
|
2024-09-14 21:17:03 +08:00
|
|
|
onCleanup(() => {
|
|
|
|
document.body.removeChild(container);
|
|
|
|
});
|
2024-09-14 20:43:44 +08:00
|
|
|
|
2024-09-14 20:21:19 +08:00
|
|
|
return (
|
|
|
|
<MediaViewer
|
|
|
|
show={viewerOpened()}
|
|
|
|
index={viewerIndex() || 0}
|
|
|
|
onIndexUpdated={setViewerIndex}
|
|
|
|
media={props.attachments}
|
2024-09-14 21:17:03 +08:00
|
|
|
onClose={() => setViewerIndex()}
|
2024-09-14 20:21:19 +08:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}, container);
|
2024-11-20 20:34:30 +08:00
|
|
|
|
|
|
|
onCleanup(dispose);
|
2024-09-14 21:17:03 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
const openViewerFor = (index: number) => {
|
|
|
|
setViewerIndex(index);
|
2024-07-14 20:28:44 +08:00
|
|
|
};
|
|
|
|
|
2024-10-08 17:43:33 +08:00
|
|
|
const columnCount = () => {
|
|
|
|
if (props.attachments.length === 1) {
|
|
|
|
return 1;
|
|
|
|
} else if (props.attachments.length % 2 === 0) {
|
|
|
|
return 2;
|
|
|
|
} else {
|
|
|
|
return 3;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-10-28 18:26:59 +08:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2024-10-29 19:35:40 +08:00
|
|
|
const itemStyle = (item: mastodon.v1.MediaAttachment) => {
|
|
|
|
const { width, height } = constraintedSize(
|
|
|
|
item.meta?.small || { width: 1, height: 1 },
|
|
|
|
{ width: 2, height: 2 },
|
|
|
|
itemMaxSize(),
|
|
|
|
);
|
|
|
|
|
2024-11-11 16:53:34 +08:00
|
|
|
const accentColor =
|
|
|
|
item.meta?.colors?.accent ??
|
|
|
|
(item.blurhash ? averageColorHex(item.blurhash) : undefined);
|
2024-10-29 19:35:40 +08:00
|
|
|
|
|
|
|
return Object.assign(
|
|
|
|
{
|
|
|
|
width: `${width}px`,
|
|
|
|
height: `${height}px`,
|
2024-10-30 13:07:55 +08:00
|
|
|
"contain-intrinsic-size": `${width}px ${height}px`,
|
2024-10-29 19:35:40 +08:00
|
|
|
},
|
|
|
|
accentColor ? { "--media-color-accent": accentColor } : {},
|
|
|
|
);
|
|
|
|
};
|
2024-11-20 21:09:14 +08:00
|
|
|
|
|
|
|
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 (
|
|
|
|
<section
|
2024-10-28 18:26:59 +08:00
|
|
|
ref={setRootRef}
|
2024-11-12 19:12:49 +08:00
|
|
|
class={`MediaAttachmentGrid ${cardStyle.cardNoPad}`}
|
2024-11-20 20:34:30 +08:00
|
|
|
classList={{
|
|
|
|
sensitive: props.sensitive,
|
2024-10-09 18:45:19 +08:00
|
|
|
}}
|
2024-11-20 20:34:30 +08:00
|
|
|
style={{ "column-count": columnCount() }}
|
|
|
|
onClick={isolateCallback}
|
2024-07-14 20:28:44 +08:00
|
|
|
>
|
2024-10-29 19:35:40 +08:00
|
|
|
<Index each={props.attachments}>
|
2024-07-14 20:28:44 +08:00
|
|
|
{(item, index) => {
|
2024-11-11 14:57:49 +08:00
|
|
|
const itemType = () => item().type;
|
2024-11-20 21:51:29 +08:00
|
|
|
|
|
|
|
const style = createMemo(() => itemStyle(item()));
|
2024-11-11 14:57:49 +08:00
|
|
|
return (
|
2024-11-20 21:51:29 +08:00
|
|
|
<Switch>
|
|
|
|
<Match when={props.sensitive && !isReveal(index)}>
|
|
|
|
<div
|
|
|
|
class="sensitive-placeholder"
|
|
|
|
style={style()}
|
|
|
|
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"
|
|
|
|
style={style()}
|
|
|
|
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}
|
|
|
|
style={style()}
|
|
|
|
data-sort={index}
|
|
|
|
data-media-type={item().type}
|
2024-11-21 23:35:56 +08:00
|
|
|
preload="metadata"
|
2024-11-20 21:51:29 +08:00
|
|
|
/>
|
|
|
|
</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}
|
|
|
|
style={style()}
|
|
|
|
data-sort={index}
|
|
|
|
data-media-type={item().type}
|
2024-11-21 23:35:56 +08:00
|
|
|
preload="metadata"
|
2024-11-20 21:51:29 +08:00
|
|
|
/>
|
|
|
|
</Match>
|
|
|
|
<Match when={itemType() === "audio"}>
|
|
|
|
<audio
|
|
|
|
src={item().url || undefined}
|
|
|
|
controls
|
|
|
|
data-sort={index}
|
|
|
|
data-media-type={item().type}
|
|
|
|
></audio>
|
|
|
|
</Match>
|
|
|
|
</Switch>
|
2024-11-11 14:57:49 +08:00
|
|
|
);
|
2024-07-14 20:28:44 +08:00
|
|
|
}}
|
2024-10-29 19:35:40 +08:00
|
|
|
</Index>
|
2024-07-14 20:28:44 +08:00
|
|
|
</section>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default MediaAttachmentGrid;
|