Compare commits
3 commits
2aa2cf21da
...
4d9c2b3aa8
Author | SHA1 | Date | |
---|---|---|---|
|
4d9c2b3aa8 | ||
|
3cacf64c8e | ||
|
9bc93cae82 |
8 changed files with 202 additions and 144 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -17,16 +17,16 @@
|
||||||
"@suid/vite-plugin": "^0.3.1",
|
"@suid/vite-plugin": "^0.3.1",
|
||||||
"@types/hammerjs": "^2.0.46",
|
"@types/hammerjs": "^2.0.46",
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^5.4.10",
|
"vite": "^5.4.11",
|
||||||
"vite-plugin-package-version": "^1.1.0",
|
"vite-plugin-package-version": "^1.1.0",
|
||||||
"vite-plugin-pwa": "^0.20.5",
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
"vite-plugin-solid": "^2.10.2",
|
"vite-plugin-solid": "^2.10.2",
|
||||||
"vite-plugin-solid-styled": "^0.11.1",
|
"vite-plugin-solid-styled": "^0.11.1",
|
||||||
"workbox-build": "^7.3.0",
|
"workbox-build": "^7.3.0",
|
||||||
"wrangler": "^3.84.1"
|
"wrangler": "^3.86.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "^0.5.7",
|
"@formatjs/intl-localematcher": "^0.5.7",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
"@solid-primitives/map": "^0.4.13",
|
"@solid-primitives/map": "^0.4.13",
|
||||||
"@solid-primitives/page-visibility": "^2.0.17",
|
"@solid-primitives/page-visibility": "^2.0.17",
|
||||||
"@solid-primitives/resize-observer": "^2.0.26",
|
"@solid-primitives/resize-observer": "^2.0.26",
|
||||||
"@solidjs/router": "^0.14.10",
|
"@solidjs/router": "^0.15.1",
|
||||||
"@suid/icons-material": "^0.8.1",
|
"@suid/icons-material": "^0.8.1",
|
||||||
"@suid/material": "^0.18.0",
|
"@suid/material": "^0.18.0",
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
|
|
27
src/timelines/MediaAttachmentGrid.css
Normal file
27
src/timelines/MediaAttachmentGrid.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.MediaAttachmentGrid {
|
||||||
|
/* Note: MeidaAttachmentGrid has hard-coded layout calcalation */
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px);
|
||||||
|
margin-right: var(--card-pad, 0);
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
> :where(img, video) {
|
||||||
|
max-height: 35vh;
|
||||||
|
min-height: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
object-fit: contain;
|
||||||
|
max-width: 100%;
|
||||||
|
background-color: 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);
|
||||||
|
contain: strict;
|
||||||
|
content-visibility: auto;
|
||||||
|
|
||||||
|
&: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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,8 +10,6 @@ import {
|
||||||
createSignal,
|
createSignal,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { css } from "solid-styled";
|
|
||||||
import tootStyle from "./toot.module.css";
|
|
||||||
import MediaViewer from "./MediaViewer";
|
import MediaViewer from "./MediaViewer";
|
||||||
import { render } from "solid-js/web";
|
import { render } from "solid-js/web";
|
||||||
import {
|
import {
|
||||||
|
@ -21,6 +19,8 @@ import {
|
||||||
import { useStore } from "@nanostores/solid";
|
import { useStore } from "@nanostores/solid";
|
||||||
import { $settings } from "../settings/stores";
|
import { $settings } from "../settings/stores";
|
||||||
import { averageColorHex } from "../platform/blurhash";
|
import { averageColorHex } from "../platform/blurhash";
|
||||||
|
import "./MediaAttachmentGrid.css";
|
||||||
|
import cardStyle from "../material/cards.module.css";
|
||||||
|
|
||||||
type ElementSize = { width: number; height: number };
|
type ElementSize = { width: number; height: number };
|
||||||
|
|
||||||
|
@ -114,13 +114,6 @@ const MediaAttachmentGrid: Component<{
|
||||||
itemMaxSize(),
|
itemMaxSize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// I don't know why mastodon does not return this
|
|
||||||
// and the condition for it to return this.
|
|
||||||
// Anyway, It is useless now.
|
|
||||||
// My hope is the FastAverageColor, but
|
|
||||||
// we may need better tool to manage the performance impact
|
|
||||||
// before using this. See #37.
|
|
||||||
// TODO: use fast average color to extract accent color
|
|
||||||
const accentColor =
|
const accentColor =
|
||||||
item.meta?.colors?.accent ??
|
item.meta?.colors?.accent ??
|
||||||
(item.blurhash ? averageColorHex(item.blurhash) : undefined);
|
(item.blurhash ? averageColorHex(item.blurhash) : undefined);
|
||||||
|
@ -134,16 +127,11 @@ const MediaAttachmentGrid: Component<{
|
||||||
accentColor ? { "--media-color-accent": accentColor } : {},
|
accentColor ? { "--media-color-accent": accentColor } : {},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
css`
|
|
||||||
.attachments {
|
|
||||||
column-count: ${columnCount().toString()};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
ref={setRootRef}
|
ref={setRootRef}
|
||||||
class={[tootStyle.tootAttachmentGrp, "attachments"].join(" ")}
|
class={`MediaAttachmentGrid ${cardStyle.cardNoPad}`}
|
||||||
|
style={{ "column-count": columnCount() }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target !== e.currentTarget) {
|
if (e.target !== e.currentTarget) {
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
|
|
|
@ -5,13 +5,11 @@ import {
|
||||||
type JSX,
|
type JSX,
|
||||||
Show,
|
Show,
|
||||||
createRenderEffect,
|
createRenderEffect,
|
||||||
createEffect,
|
|
||||||
createMemo,
|
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import tootStyle from "./toot.module.css";
|
import tootStyle from "./toot.module.css";
|
||||||
import { formatRelative } from "date-fns";
|
import { formatRelative } from "date-fns";
|
||||||
import Img from "../material/Img.js";
|
import Img from "../material/Img.js";
|
||||||
import { Body1, Body2, Title } from "../material/typography.js";
|
import { Body2 } from "../material/typography.js";
|
||||||
import { css } from "solid-styled";
|
import { css } from "solid-styled";
|
||||||
import {
|
import {
|
||||||
BookmarkAddOutlined,
|
BookmarkAddOutlined,
|
||||||
|
@ -30,13 +28,12 @@ import { Divider } from "@suid/material";
|
||||||
import cardStyle from "../material/cards.module.css";
|
import cardStyle from "../material/cards.module.css";
|
||||||
import Button from "../material/Button.js";
|
import Button from "../material/Button.js";
|
||||||
import MediaAttachmentGrid from "./MediaAttachmentGrid.js";
|
import MediaAttachmentGrid from "./MediaAttachmentGrid.js";
|
||||||
import Color from "colorjs.io";
|
|
||||||
import { useDateFnLocale } from "../platform/i18n";
|
import { useDateFnLocale } from "../platform/i18n";
|
||||||
import { canShare, share } from "../platform/share";
|
import { canShare, share } from "../platform/share";
|
||||||
import { makeAcctText, useDefaultSession } from "../masto/clients";
|
import { makeAcctText, useDefaultSession } from "../masto/clients";
|
||||||
import TootContent from "./toot-components/TootContent";
|
import TootContent from "./toot-components/TootContent";
|
||||||
import BoostIcon from "./toot-components/BoostIcon";
|
import BoostIcon from "./toot-components/BoostIcon";
|
||||||
import { averageColorHex } from "../platform/blurhash";
|
import PreviewCard from "./toot-components/PreviewCard";
|
||||||
|
|
||||||
type TootActionGroupProps<T extends mastodon.v1.Status> = {
|
type TootActionGroupProps<T extends mastodon.v1.Status> = {
|
||||||
onRetoot?: (value: T) => void;
|
onRetoot?: (value: T) => void;
|
||||||
|
@ -186,95 +183,6 @@ function TootAuthorGroup(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TootPreviewCard(props: {
|
|
||||||
src: mastodon.v1.PreviewCard;
|
|
||||||
alwaysCompact?: boolean;
|
|
||||||
}) {
|
|
||||||
let root: HTMLAnchorElement;
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (props.alwaysCompact) {
|
|
||||||
root.classList.add(tootStyle.compact);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!props.src.width) return;
|
|
||||||
const width = root.getBoundingClientRect().width;
|
|
||||||
if (width > props.src.width) {
|
|
||||||
root.classList.add(tootStyle.compact);
|
|
||||||
} else {
|
|
||||||
root.classList.remove(tootStyle.compact);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const imgAverageColor = createMemo(() => {
|
|
||||||
if (!props.src.image) return;
|
|
||||||
return new Color(averageColorHex(props.src.blurhash));
|
|
||||||
});
|
|
||||||
|
|
||||||
const prefersWhiteText = createMemo(() => {
|
|
||||||
const oc = imgAverageColor();
|
|
||||||
if (!oc) return;
|
|
||||||
const colorWhite = new Color("white");
|
|
||||||
|
|
||||||
return colorWhite.luminance / oc.luminance > 3.5;
|
|
||||||
});
|
|
||||||
|
|
||||||
const focusSurfaceColor = createMemo(() => {
|
|
||||||
const oc = imgAverageColor();
|
|
||||||
if (!oc) return;
|
|
||||||
if (prefersWhiteText()) {
|
|
||||||
return new Color(oc).darken(0.2);
|
|
||||||
} else {
|
|
||||||
return new Color(oc).lighten(0.2);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const textColorName = createMemo(() => {
|
|
||||||
const useWhiteText = prefersWhiteText();
|
|
||||||
if (typeof useWhiteText === "undefined") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return useWhiteText ? "white" : "black";
|
|
||||||
});
|
|
||||||
|
|
||||||
const secondaryTextColor = createMemo(() => {
|
|
||||||
const tcn = textColorName();
|
|
||||||
if (!tcn) return;
|
|
||||||
const tc = new Color(tcn);
|
|
||||||
tc.alpha = 0.75;
|
|
||||||
return tc;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
ref={root!}
|
|
||||||
class={tootStyle.previewCard}
|
|
||||||
href={props.src.url}
|
|
||||||
target="_blank"
|
|
||||||
referrerPolicy="unsafe-url"
|
|
||||||
style={{
|
|
||||||
"--tutu-color-surface": imgAverageColor()?.toString(),
|
|
||||||
"--tutu-color-surface-d": focusSurfaceColor()?.toString(),
|
|
||||||
"--tutu-color-on-surface": textColorName(),
|
|
||||||
"--tutu-color-secondary-text-on-surface":
|
|
||||||
secondaryTextColor()?.toString(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={props.src.image}>
|
|
||||||
<img
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
src={props.src.image!}
|
|
||||||
width={props.src.width || undefined}
|
|
||||||
height={props.src.height || undefined}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
<Title component="h1">{props.src.title}</Title>
|
|
||||||
<Body1 component="p">{props.src.description}</Body1>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* find bottom-to-top the element with `data-action`.
|
* find bottom-to-top the element with `data-action`.
|
||||||
*/
|
*/
|
||||||
|
@ -420,7 +328,7 @@ const RegularToot: Component<TootCardProps> = (props) => {
|
||||||
class={tootStyle.tootContent}
|
class={tootStyle.tootContent}
|
||||||
/>
|
/>
|
||||||
<Show when={toot().card}>
|
<Show when={toot().card}>
|
||||||
<TootPreviewCard src={toot().card!} />
|
<PreviewCard src={toot().card!} />
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={toot().mediaAttachments.length > 0}>
|
<Show when={toot().mediaAttachments.length > 0}>
|
||||||
<MediaAttachmentGrid attachments={toot().mediaAttachments} />
|
<MediaAttachmentGrid attachments={toot().mediaAttachments} />
|
||||||
|
|
67
src/timelines/toot-components/PreviewCard.css
Normal file
67
src/timelines/toot-components/PreviewCard.css
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
.PreviewCard {
|
||||||
|
display: block;
|
||||||
|
border: 1px solid #eeeeee;
|
||||||
|
background-color: var(--tutu-color-surface);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
color: var(--tutu-color-secondary-text-on-surface);
|
||||||
|
transition: color 220ms var(--tutu-anim-curve-std), background-color 220ms var(--tutu-anim-curve-std);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
>img {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
background-color: var(--tutu-color-surface-d);
|
||||||
|
color: var(--tutu-color-on-surface);
|
||||||
|
|
||||||
|
>h1 {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
>h1 {
|
||||||
|
color: var(--tutu-color-on-surface);
|
||||||
|
max-height: calc(4 * var(--title-line-height) * var(--title-size));
|
||||||
|
}
|
||||||
|
|
||||||
|
>p {
|
||||||
|
max-height: calc(8 * var(--body-line-height) * var(--body-size));
|
||||||
|
}
|
||||||
|
|
||||||
|
>h1,
|
||||||
|
>p {
|
||||||
|
margin-left: 16px;
|
||||||
|
margin-right: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(10%, 30%) 1fr;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
padding-top: 8px;
|
||||||
|
|
||||||
|
>img:first-child {
|
||||||
|
grid-row: 1 / 3;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
>h1,
|
||||||
|
>p {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
97
src/timelines/toot-components/PreviewCard.tsx
Normal file
97
src/timelines/toot-components/PreviewCard.tsx
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import Color from "colorjs.io";
|
||||||
|
import type { mastodon } from "masto";
|
||||||
|
import { createEffect, createMemo, Show } from "solid-js";
|
||||||
|
import { Title, Body1 } from "../../material/typography";
|
||||||
|
import { averageColorHex } from "../../platform/blurhash";
|
||||||
|
import "./PreviewCard.css";
|
||||||
|
|
||||||
|
export function PreviewCard(props: {
|
||||||
|
src: mastodon.v1.PreviewCard;
|
||||||
|
alwaysCompact?: boolean;
|
||||||
|
}) {
|
||||||
|
let root: HTMLAnchorElement;
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.alwaysCompact) {
|
||||||
|
root.classList.add("compact");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!props.src.width) return;
|
||||||
|
const width = root.getBoundingClientRect().width;
|
||||||
|
if (width > props.src.width) {
|
||||||
|
root.classList.add("compact");
|
||||||
|
} else {
|
||||||
|
root.classList.remove("compact");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const imgAverageColor = createMemo(() => {
|
||||||
|
if (!props.src.image) return;
|
||||||
|
return new Color(averageColorHex(props.src.blurhash));
|
||||||
|
});
|
||||||
|
|
||||||
|
const prefersWhiteText = createMemo(() => {
|
||||||
|
const oc = imgAverageColor();
|
||||||
|
if (!oc) return;
|
||||||
|
const colorWhite = new Color("white");
|
||||||
|
|
||||||
|
return colorWhite.luminance / oc.luminance > 3.5;
|
||||||
|
});
|
||||||
|
|
||||||
|
const focusSurfaceColor = createMemo(() => {
|
||||||
|
const oc = imgAverageColor();
|
||||||
|
if (!oc) return;
|
||||||
|
if (prefersWhiteText()) {
|
||||||
|
return new Color(oc).darken(0.2);
|
||||||
|
} else {
|
||||||
|
return new Color(oc).lighten(0.2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const textColorName = createMemo(() => {
|
||||||
|
const useWhiteText = prefersWhiteText();
|
||||||
|
if (typeof useWhiteText === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return useWhiteText ? "white" : "black";
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondaryTextColor = createMemo(() => {
|
||||||
|
const tcn = textColorName();
|
||||||
|
if (!tcn) return;
|
||||||
|
const tc = new Color(tcn);
|
||||||
|
tc.alpha = 0.75;
|
||||||
|
return tc;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
ref={root!}
|
||||||
|
class={"PreviewCard"}
|
||||||
|
href={props.src.url}
|
||||||
|
target="_blank"
|
||||||
|
referrerPolicy="unsafe-url"
|
||||||
|
style={{
|
||||||
|
"--tutu-color-surface": imgAverageColor()?.toString(),
|
||||||
|
"--tutu-color-surface-d": focusSurfaceColor()?.toString(),
|
||||||
|
"--tutu-color-on-surface": textColorName(),
|
||||||
|
"--tutu-color-secondary-text-on-surface":
|
||||||
|
secondaryTextColor()?.toString(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={props.src.image}>
|
||||||
|
<img
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={props.src.image!}
|
||||||
|
width={props.src.width || undefined}
|
||||||
|
height={props.src.height || undefined}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<Title component="h1">{props.src.title}</Title>
|
||||||
|
<Body1 component="p">{props.src.description}</Body1>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PreviewCard;
|
|
@ -238,35 +238,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tootAttachmentGrp {
|
|
||||||
/* Note: MeidaAttachmentGrid has hard-coded layout calcalation */
|
|
||||||
composes: cardNoPad from "../material/cards.module.css";
|
|
||||||
margin-top: 1em;
|
|
||||||
margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px);
|
|
||||||
margin-right: var(--card-pad, 0);
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
> :where(img, video) {
|
|
||||||
max-height: 35vh;
|
|
||||||
min-height: 40px;
|
|
||||||
min-width: 40px;
|
|
||||||
object-fit: contain;
|
|
||||||
max-width: 100%;
|
|
||||||
background-color: 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);
|
|
||||||
contain: strict;
|
|
||||||
content-visibility: auto;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 8px solid var(--media-color-accent, var(--tutu-color-surface-d));
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tootBottomActionGrp {
|
.tootBottomActionGrp {
|
||||||
composes: cardGutSkip from "../material/cards.module.css";
|
composes: cardGutSkip from "../material/cards.module.css";
|
||||||
padding-block: calc((var(--card-gut) - 10px) / 2);
|
padding-block: calc((var(--card-gut) - 10px) / 2);
|
||||||
|
|
Loading…
Reference in a new issue