Compare commits
5 commits
66bded813d
...
726abbe893
Author | SHA1 | Date | |
---|---|---|---|
|
726abbe893 | ||
|
7bb6c0b826 | ||
|
93f98059cd | ||
|
81bf27df63 | ||
|
d1c073e33e |
8 changed files with 114 additions and 33 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -36,6 +36,7 @@
|
||||||
"@solid-primitives/i18n": "^2.1.1",
|
"@solid-primitives/i18n": "^2.1.1",
|
||||||
"@solid-primitives/intersection-observer": "^2.1.6",
|
"@solid-primitives/intersection-observer": "^2.1.6",
|
||||||
"@solid-primitives/map": "^0.4.13",
|
"@solid-primitives/map": "^0.4.13",
|
||||||
|
"@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.14.10",
|
||||||
"@suid/icons-material": "^0.8.1",
|
"@suid/icons-material": "^0.8.1",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { usePageVisibility } from "@solid-primitives/page-visibility";
|
||||||
import {
|
import {
|
||||||
Accessor,
|
Accessor,
|
||||||
createContext,
|
createContext,
|
||||||
|
@ -15,19 +16,30 @@ export const TimeSourceProvider = TimeSourceContext.Provider;
|
||||||
export function createTimeSource() {
|
export function createTimeSource() {
|
||||||
let id: ReturnType<typeof setTimeout> | undefined;
|
let id: ReturnType<typeof setTimeout> | undefined;
|
||||||
const [get, set] = createSignal(new Date());
|
const [get, set] = createSignal(new Date());
|
||||||
|
const visible = usePageVisibility();
|
||||||
|
|
||||||
createRenderEffect(() =>
|
const cancelTimer = () => {
|
||||||
untrack(() => {
|
|
||||||
id = setTimeout(() => {
|
|
||||||
set(new Date());
|
|
||||||
}, 30 * 1000);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
if (typeof id !== "undefined") {
|
if (typeof id !== "undefined") {
|
||||||
clearInterval(id);
|
clearInterval(id);
|
||||||
}
|
}
|
||||||
|
id = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetTimer = () => {
|
||||||
|
cancelTimer();
|
||||||
|
set(new Date());
|
||||||
|
id = setTimeout(() => {
|
||||||
|
set(new Date());
|
||||||
|
}, 30 * 1000); // refresh rate: 30s
|
||||||
|
};
|
||||||
|
|
||||||
|
createRenderEffect(() => {
|
||||||
|
onCleanup(cancelTimer);
|
||||||
|
if (visible()) {
|
||||||
|
resetTimer();
|
||||||
|
} else {
|
||||||
|
console.debug("createTimeSource: page is invisible, cancel the timer")
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return get;
|
return get;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
AppBar,
|
AppBar,
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
|
CircularProgress,
|
||||||
Divider,
|
Divider,
|
||||||
IconButton,
|
IconButton,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
|
@ -102,7 +103,7 @@ const Profile: Component = () => {
|
||||||
const avatarImg = () => profile()?.avatar;
|
const avatarImg = () => profile()?.avatar;
|
||||||
const displayName = () =>
|
const displayName = () =>
|
||||||
resolveCustomEmoji(profile()?.displayName || "", profile()?.emojis ?? []);
|
resolveCustomEmoji(profile()?.displayName || "", profile()?.emojis ?? []);
|
||||||
const fullUsername = () => `@${profile()?.acct ?? ""}`; // TODO: full user name
|
const fullUsername = () => (profile()?.acct ? `@${profile()!.acct!}` : ""); // TODO: full user name
|
||||||
const description = () => profile()?.note;
|
const description = () => profile()?.note;
|
||||||
|
|
||||||
css`
|
css`
|
||||||
|
@ -231,7 +232,12 @@ const Profile: Component = () => {
|
||||||
<ListItemText>Mention {profile()?.displayName || ""}...</ListItemText>
|
<ListItemText>Mention {profile()?.displayName || ""}...</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
<MenuItem component={"a"} href={profile()?.url} target="_blank" rel="noopener noreferrer">
|
<MenuItem
|
||||||
|
component={"a"}
|
||||||
|
href={profile()?.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<OpenInBrowser />
|
<OpenInBrowser />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -307,6 +313,7 @@ const Profile: Component = () => {
|
||||||
createRenderEffect(() => (e.innerHTML = description() || ""))
|
createRenderEffect(() => (e.innerHTML = description() || ""))
|
||||||
}
|
}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<table class="acct-fields">
|
<table class="acct-fields">
|
||||||
<tbody>
|
<tbody>
|
||||||
<For each={profile()?.fields ?? []}>
|
<For each={profile()?.fields ?? []}>
|
||||||
|
@ -365,8 +372,11 @@ const Profile: Component = () => {
|
||||||
size="large"
|
size="large"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={[refetchRecentToots, "prev"]}
|
onClick={[refetchRecentToots, "prev"]}
|
||||||
|
disabled={recentTootChunk.loading}
|
||||||
>
|
>
|
||||||
<ExpandMore />
|
<Show when={recentTootChunk.loading} fallback={<ExpandMore />}>
|
||||||
|
<CircularProgress sx={{ width: "24px", height: "24px" }} />
|
||||||
|
</Show>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -249,7 +249,6 @@ const Home: ParentComponent = (props) => {
|
||||||
<div>
|
<div>
|
||||||
<TrendTimelinePanel
|
<TrendTimelinePanel
|
||||||
client={client()}
|
client={client()}
|
||||||
prefetch={prefetching()}
|
|
||||||
openFullScreenToot={openFullScreenToot}
|
openFullScreenToot={openFullScreenToot}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,29 +2,52 @@ import type { mastodon } from "masto";
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
For,
|
For,
|
||||||
createEffect,
|
createMemo,
|
||||||
createRenderEffect,
|
createRenderEffect,
|
||||||
createSignal,
|
createSignal,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
onMount,
|
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { css } from "solid-styled";
|
import { css } from "solid-styled";
|
||||||
import tootStyle from "./toot.module.css";
|
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 { useWindowSize } from "@solid-primitives/resize-observer";
|
import {
|
||||||
|
createElementSize,
|
||||||
|
useWindowSize,
|
||||||
|
} from "@solid-primitives/resize-observer";
|
||||||
import { useStore } from "@nanostores/solid";
|
import { useStore } from "@nanostores/solid";
|
||||||
import { $settings } from "../settings/stores";
|
import { $settings } from "../settings/stores";
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const MediaAttachmentGrid: Component<{
|
const MediaAttachmentGrid: Component<{
|
||||||
attachments: mastodon.v1.MediaAttachment[];
|
attachments: mastodon.v1.MediaAttachment[];
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
let rootRef: HTMLElement;
|
const [rootRef, setRootRef] = createSignal<HTMLElement>();
|
||||||
const [viewerIndex, setViewerIndex] = createSignal<number>();
|
const [viewerIndex, setViewerIndex] = createSignal<number>();
|
||||||
const viewerOpened = () => typeof viewerIndex() !== "undefined";
|
const viewerOpened = () => typeof viewerIndex() !== "undefined";
|
||||||
const windowSize = useWindowSize();
|
|
||||||
const vh35 = () => Math.floor(windowSize.height * 0.35);
|
|
||||||
const settings = useStore($settings);
|
const settings = useStore($settings);
|
||||||
|
const windowSize = useWindowSize();
|
||||||
|
|
||||||
createRenderEffect((lastDispose?: () => void) => {
|
createRenderEffect((lastDispose?: () => void) => {
|
||||||
lastDispose?.();
|
lastDispose?.();
|
||||||
|
@ -64,6 +87,22 @@ const MediaAttachmentGrid: Component<{
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
css`
|
css`
|
||||||
.attachments {
|
.attachments {
|
||||||
column-count: ${columnCount().toString()};
|
column-count: ${columnCount().toString()};
|
||||||
|
@ -71,7 +110,7 @@ const MediaAttachmentGrid: Component<{
|
||||||
`;
|
`;
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
ref={rootRef!}
|
ref={setRootRef}
|
||||||
class={[tootStyle.tootAttachmentGrp, "attachments"].join(" ")}
|
class={[tootStyle.tootAttachmentGrp, "attachments"].join(" ")}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target !== e.currentTarget) {
|
if (e.target !== e.currentTarget) {
|
||||||
|
@ -89,19 +128,33 @@ const MediaAttachmentGrid: Component<{
|
||||||
// before using this. See #37.
|
// before using this. See #37.
|
||||||
// TODO: use fast average color to extract accent color
|
// TODO: use fast average color to extract accent color
|
||||||
const accentColor = item.meta?.colors?.accent;
|
const accentColor = item.meta?.colors?.accent;
|
||||||
|
const { width, height } = item.meta?.small || {};
|
||||||
|
|
||||||
|
const size = () => {
|
||||||
|
return constraintedSize(
|
||||||
|
item.meta?.small || { width: 1, height: 1 },
|
||||||
|
{ width: 2, height: 2 },
|
||||||
|
itemMaxSize(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case "image":
|
case "image":
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={item.previewUrl}
|
src={item.previewUrl}
|
||||||
width={item.meta?.small?.width}
|
width={width}
|
||||||
height={item.meta?.small?.height}
|
height={height}
|
||||||
alt={item.description || undefined}
|
alt={item.description || undefined}
|
||||||
onClick={[openViewerFor, index()]}
|
onClick={[openViewerFor, index()]}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={
|
style={Object.assign(
|
||||||
accentColor ? { "--media-color-accent": accentColor } : {}
|
{
|
||||||
}
|
width: `${size().width}px`,
|
||||||
|
height: `${size().height}px`,
|
||||||
|
},
|
||||||
|
accentColor ? { "--media-color-accent": accentColor } : {},
|
||||||
|
)}
|
||||||
></img>
|
></img>
|
||||||
);
|
);
|
||||||
case "video":
|
case "video":
|
||||||
|
@ -112,8 +165,8 @@ const MediaAttachmentGrid: Component<{
|
||||||
playsinline={settings().autoPlayVideos ? true : undefined}
|
playsinline={settings().autoPlayVideos ? true : undefined}
|
||||||
controls
|
controls
|
||||||
poster={item.previewUrl}
|
poster={item.previewUrl}
|
||||||
width={item.meta?.small?.width}
|
width={width}
|
||||||
height={item.meta?.small?.height}
|
height={height}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "gifv":
|
case "gifv":
|
||||||
|
@ -125,8 +178,8 @@ const MediaAttachmentGrid: Component<{
|
||||||
playsinline /* or safari on iOS will play in full-screen */
|
playsinline /* or safari on iOS will play in full-screen */
|
||||||
loop
|
loop
|
||||||
poster={item.previewUrl}
|
poster={item.previewUrl}
|
||||||
width={item.meta?.small?.width}
|
width={width}
|
||||||
height={item.meta?.small?.height}
|
height={height}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -223,6 +223,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tootAttachmentGrp {
|
.tootAttachmentGrp {
|
||||||
|
/* Note: MeidaAttachmentGrid has hard-coded layout calcalation */
|
||||||
composes: cardNoPad from "../material/cards.module.css";
|
composes: cardNoPad from "../material/cards.module.css";
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px);
|
margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px);
|
||||||
|
@ -233,14 +234,12 @@
|
||||||
max-height: 35vh;
|
max-height: 35vh;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
background-color: var(--tutu-color-surface-d);
|
background-color: var(--tutu-color-surface-d);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
border: 1px solid var(--tutu-color-on-surface-d);
|
border: 1px solid var(--tutu-color-surface-d);
|
||||||
transition: outline-width 60ms var(--tutu-anim-curve-std), z-index 60ms var(--tutu-anim-curve-std);
|
transition: outline-width 60ms var(--tutu-anim-curve-std), border-color 60ms var(--tutu-anim-curve-std);
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
|
@ -248,6 +247,7 @@
|
||||||
/* but our infra is not prepared for this. The average color thing is slow */
|
/* but our infra is not prepared for this. The average color thing is slow */
|
||||||
/* and we need further managing to control its performance impact. */
|
/* and we need further managing to control its performance impact. */
|
||||||
outline: 8px solid var(--media-color-accent, var(--tutu-color-surface-d));
|
outline: 8px solid var(--media-color-accent, var(--tutu-color-surface-d));
|
||||||
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,8 @@ export default defineConfig(({ mode }) => {
|
||||||
? `${serverHttpCertBase}.crt`
|
? `${serverHttpCertBase}.crt`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const isTestBuild = ["development", "staging"].includes(mode);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [
|
plugins: [
|
||||||
suid(),
|
suid(),
|
||||||
|
@ -101,6 +103,10 @@ export default defineConfig(({ mode }) => {
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
|
esbuild: {
|
||||||
|
pure: isTestBuild ? undefined : ["console.debug", "console.trace"],
|
||||||
|
drop: isTestBuild ? undefined : ["debugger"],
|
||||||
|
},
|
||||||
define: {
|
define: {
|
||||||
"import.meta.env.BUILT_AT": `"${new Date().toISOString()}"`,
|
"import.meta.env.BUILT_AT": `"${new Date().toISOString()}"`,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue