MediaAttachmentGrid: moved into toots
This commit is contained in:
parent
cac0abeb6b
commit
ade7df2234
3 changed files with 5 additions and 5 deletions
40
src/timelines/toots/MediaAttachmentGrid.css
Normal file
40
src/timelines/toots/MediaAttachmentGrid.css
Normal file
|
@ -0,0 +1,40 @@
|
|||
.MediaAttachmentGrid {
|
||||
/* Note: MeidaAttachmentGrid has hard-coded layout calcalation */
|
||||
margin-top: 1em;
|
||||
margin-left: calc(var(--card-pad, 0) + 8px);
|
||||
margin-right: var(--card-pad, 0);
|
||||
gap: 4px;
|
||||
contain: layout style;
|
||||
|
||||
> :where(img, video, .sensitive-placeholder) {
|
||||
max-height: 35vh;
|
||||
min-height: 40px;
|
||||
min-width: 40px;
|
||||
max-width: 100%;
|
||||
contain: strict;
|
||||
content-visibility: auto;
|
||||
background-color: var(--media-color-accent, var(--tutu-color-surface-d));
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--tutu-color-surface-d);
|
||||
transition: outline-width 60ms var(--tutu-anim-curve-std), border-color 60ms var(--tutu-anim-curve-std);
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
outline: 8px solid var(--media-color-accent, var(--tutu-color-surface-d));
|
||||
border-color: var(--media-color-accent, var(--tutu-color-surface-d));
|
||||
}
|
||||
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
>.sensitive-placeholder {
|
||||
display: inline-flex;
|
||||
display: inline flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
:where(.thread-top, .thread-mid) > .MediaAttachmentGrid {
|
||||
margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px);
|
||||
}
|
245
src/timelines/toots/MediaAttachmentGrid.tsx
Normal file
245
src/timelines/toots/MediaAttachmentGrid.tsx
Normal file
|
@ -0,0 +1,245 @@
|
|||
import type { mastodon } from "masto";
|
||||
import {
|
||||
type Component,
|
||||
Index,
|
||||
Match,
|
||||
Switch,
|
||||
createMemo,
|
||||
createRenderEffect,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
untrack,
|
||||
} from "solid-js";
|
||||
import MediaViewer from "../MediaViewer";
|
||||
import { render } from "solid-js/web";
|
||||
import {
|
||||
createElementSize,
|
||||
useWindowSize,
|
||||
} from "@solid-primitives/resize-observer";
|
||||
import { useStore } from "@nanostores/solid";
|
||||
import { $settings } from "../../settings/stores";
|
||||
import { averageColorHex } from "../../platform/blurhash";
|
||||
import "./MediaAttachmentGrid.css";
|
||||
import cardStyle from "../../material/cards.module.css";
|
||||
import { Preview } from "@suid/icons-material";
|
||||
import { IconButton } from "@suid/material";
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
const MediaAttachmentGrid: Component<{
|
||||
attachments: mastodon.v1.MediaAttachment[];
|
||||
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 openViewerFor = (index: number) => {
|
||||
setViewerIndex(index);
|
||||
};
|
||||
|
||||
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`,
|
||||
"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]);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<section
|
||||
ref={setRootRef}
|
||||
class={`MediaAttachmentGrid ${cardStyle.cardNoPad}`}
|
||||
classList={{
|
||||
sensitive: props.sensitive,
|
||||
}}
|
||||
style={{ "column-count": columnCount() }}
|
||||
onClick={isolateCallback}
|
||||
>
|
||||
<Index each={props.attachments}>
|
||||
{(item, index) => {
|
||||
const itemType = () => item().type;
|
||||
|
||||
const style = createMemo(() => itemStyle(item()));
|
||||
return (
|
||||
<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}
|
||||
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}
|
||||
style={style()}
|
||||
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>
|
||||
);
|
||||
}}
|
||||
</Index>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediaAttachmentGrid;
|
Loading…
Add table
Add a link
Reference in a new issue