Compare commits
4 commits
35f51db294
...
b133a9b9a7
Author | SHA1 | Date | |
---|---|---|---|
|
b133a9b9a7 | ||
|
cf8a7b15b6 | ||
|
29073d4303 | ||
|
5c150a6aa3 |
8 changed files with 164 additions and 19 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -36,7 +36,9 @@
|
||||||
"@suid/icons-material": "^0.7.0",
|
"@suid/icons-material": "^0.7.0",
|
||||||
"@suid/material": "^0.16.0",
|
"@suid/material": "^0.16.0",
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
|
"colorjs.io": "^0.5.2",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"fast-average-color": "^9.4.0",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"masto": "^6.8.0",
|
"masto": "^6.8.0",
|
||||||
"nanostores": "^0.9.5",
|
"nanostores": "^0.9.5",
|
||||||
|
|
|
@ -43,7 +43,7 @@ const MOVE_SPEED = 1400; // 1400px/s, bottom sheet is big and a bit heavier than
|
||||||
const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
|
const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
|
||||||
let element: HTMLDialogElement;
|
let element: HTMLDialogElement;
|
||||||
let animation: Animation | undefined;
|
let animation: Animation | undefined;
|
||||||
const hero = useHeroSignal(HERO);
|
const [hero, setHero] = useHeroSignal(HERO);
|
||||||
const [cache, setCache] = createSignal<ResolvedChildren | undefined>();
|
const [cache, setCache] = createSignal<ResolvedChildren | undefined>();
|
||||||
const ochildren = children(() => props.children);
|
const ochildren = children(() => props.children);
|
||||||
|
|
||||||
|
@ -70,11 +70,13 @@ const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
|
||||||
const animation = animateHero(startRect, endRect, element, true);
|
const animation = animateHero(startRect, endRect, element, true);
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
element.close();
|
element.close();
|
||||||
|
setHero();
|
||||||
};
|
};
|
||||||
animation.addEventListener("finish", onClose);
|
animation.addEventListener("finish", onClose);
|
||||||
animation.addEventListener("cancel", onClose);
|
animation.addEventListener("cancel", onClose);
|
||||||
} else {
|
} else {
|
||||||
element.close();
|
element.close();
|
||||||
|
setHero();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,9 @@ export type HeroSource = {
|
||||||
[key: string | symbol | number]: DOMRect | undefined;
|
[key: string | symbol | number]: DOMRect | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeroSourceContext = createContext<Signal<HeroSource>>(/* __@PURE__ */undefined);
|
const HeroSourceContext = createContext<Signal<HeroSource>>(
|
||||||
|
/* __@PURE__ */ undefined,
|
||||||
|
);
|
||||||
|
|
||||||
export const HeroSourceProvider = HeroSourceContext.Provider;
|
export const HeroSourceProvider = HeroSourceContext.Provider;
|
||||||
|
|
||||||
|
@ -25,7 +27,7 @@ function useHeroSource() {
|
||||||
*/
|
*/
|
||||||
export function useHeroSignal(
|
export function useHeroSignal(
|
||||||
key: string | symbol | number,
|
key: string | symbol | number,
|
||||||
): Accessor<DOMRect | undefined> {
|
): Signal<DOMRect | undefined> {
|
||||||
const source = useHeroSource();
|
const source = useHeroSource();
|
||||||
if (source) {
|
if (source) {
|
||||||
const [get, set] = createSignal<DOMRect>();
|
const [get, set] = createSignal<DOMRect>();
|
||||||
|
@ -42,8 +44,8 @@ export function useHeroSignal(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return get;
|
return [get, set];
|
||||||
} else {
|
} else {
|
||||||
return () => undefined;
|
return [() => undefined, () => undefined];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ const CompactToot: Component<CompactTootProps> = (props) => {
|
||||||
const toot = () => props.status;
|
const toot = () => props.status;
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
class={[tootStyle.compact, props.class || ""].join(" ")}
|
class={[tootStyle.toot, tootStyle.compact, props.class || ""].join(" ")}
|
||||||
lang={toot().language || undefined}
|
lang={toot().language || undefined}
|
||||||
>
|
>
|
||||||
<Img
|
<Img
|
||||||
|
|
|
@ -146,7 +146,7 @@ const TimelinePanel: Component<{
|
||||||
expanded={item.id === expandedThreadId() ? 1 : 0}
|
expanded={item.id === expandedThreadId() ? 1 : 0}
|
||||||
onExpandChange={(x) => {
|
onExpandChange={(x) => {
|
||||||
if (item.id !== expandedThreadId()) {
|
if (item.id !== expandedThreadId()) {
|
||||||
setExpandedThreadId(item.id);
|
setExpandedThreadId((x) => (x ? undefined : item.id));
|
||||||
} else if (x === 2) {
|
} else if (x === 2) {
|
||||||
props.openFullScreenToot(item, element);
|
props.openFullScreenToot(item, element);
|
||||||
}
|
}
|
||||||
|
@ -198,8 +198,16 @@ const Home: ParentComponent = (props) => {
|
||||||
const settings$ = useStore($settings);
|
const settings$ = useStore($settings);
|
||||||
|
|
||||||
const [profiles] = useSignedInProfiles();
|
const [profiles] = useSignedInProfiles();
|
||||||
const profile = () => profiles()[0].inf;
|
const profile = () => {
|
||||||
const client = () => profiles()[0].client;
|
const all = profiles();
|
||||||
|
if (all.length > 0) {
|
||||||
|
return all[0].inf;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const client = () => {
|
||||||
|
const all = profiles();
|
||||||
|
return all?.[0]?.client;
|
||||||
|
};
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [heroSrc, setHeroSrc] = createSignal<HeroSource>({});
|
const [heroSrc, setHeroSrc] = createSignal<HeroSource>({});
|
||||||
|
|
|
@ -5,11 +5,19 @@ import {
|
||||||
type JSX,
|
type JSX,
|
||||||
Show,
|
Show,
|
||||||
createRenderEffect,
|
createRenderEffect,
|
||||||
|
createSignal,
|
||||||
|
createEffect,
|
||||||
} 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 { Body2 } from "../material/typography.js";
|
import {
|
||||||
|
Body1,
|
||||||
|
Body2,
|
||||||
|
Caption,
|
||||||
|
Subheading,
|
||||||
|
Title,
|
||||||
|
} from "../material/typography.js";
|
||||||
import { css } from "solid-styled";
|
import { css } from "solid-styled";
|
||||||
import {
|
import {
|
||||||
BookmarkAddOutlined,
|
BookmarkAddOutlined,
|
||||||
|
@ -26,6 +34,8 @@ 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 { FastAverageColor } from "fast-average-color";
|
||||||
|
import Color from "colorjs.io";
|
||||||
|
|
||||||
type TootContentViewProps = {
|
type TootContentViewProps = {
|
||||||
source?: string;
|
source?: string;
|
||||||
|
@ -187,6 +197,67 @@ function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TootPreviewCard(props: { src: mastodon.v1.PreviewCard }) {
|
||||||
|
let root: HTMLAnchorElement;
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
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 onImgLoad = (event: Event & { currentTarget: HTMLImageElement }) => {
|
||||||
|
// TODO: better extraction algorithm
|
||||||
|
// I'd like to use a pattern panel and match one in the panel from the extracted color
|
||||||
|
const fac = new FastAverageColor();
|
||||||
|
const result = fac.getColor(event.currentTarget);
|
||||||
|
if (result.error) {
|
||||||
|
console.error(result.error);
|
||||||
|
fac.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.style.setProperty("--tutu-color-surface", result.hex);
|
||||||
|
const focusSurface = result.isDark
|
||||||
|
? new Color(result.hex).darken(0.2)
|
||||||
|
: new Color(result.hex).lighten(0.2);
|
||||||
|
root.style.setProperty("--tutu-color-surface-d", focusSurface.toString());
|
||||||
|
const textColor = result.isDark ? "white" : "black";
|
||||||
|
const secondaryTextColor = new Color(textColor);
|
||||||
|
secondaryTextColor.alpha = 0.75;
|
||||||
|
root.style.setProperty("--tutu-color-on-surface", textColor);
|
||||||
|
root.style.setProperty(
|
||||||
|
"--tutu-color-secondary-text-on-surface",
|
||||||
|
secondaryTextColor.toString(),
|
||||||
|
);
|
||||||
|
fac.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
ref={root!}
|
||||||
|
class={tootStyle.previewCard}
|
||||||
|
href={props.src.url}
|
||||||
|
target="_blank"
|
||||||
|
referrerPolicy="unsafe-url"
|
||||||
|
>
|
||||||
|
<Show when={props.src.image}>
|
||||||
|
<img
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={props.src.image!}
|
||||||
|
onLoad={onImgLoad}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<Title component="h1">{props.src.title}</Title>
|
||||||
|
<Body1 component="p">{props.src.description}</Body1>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const RegularToot: Component<TootCardProps> = (props) => {
|
const RegularToot: Component<TootCardProps> = (props) => {
|
||||||
let rootRef: HTMLElement;
|
let rootRef: HTMLElement;
|
||||||
const [managed, managedActionGroup, rest] = splitProps(
|
const [managed, managedActionGroup, rest] = splitProps(
|
||||||
|
@ -241,6 +312,9 @@ const RegularToot: Component<TootCardProps> = (props) => {
|
||||||
emojis={toot().emojis}
|
emojis={toot().emojis}
|
||||||
class={tootStyle.tootContent}
|
class={tootStyle.tootContent}
|
||||||
/>
|
/>
|
||||||
|
<Show when={toot().card}>
|
||||||
|
<TootPreviewCard src={toot().card!} />
|
||||||
|
</Show>
|
||||||
<Show when={toot().mediaAttachments.length > 0}>
|
<Show when={toot().mediaAttachments.length > 0}>
|
||||||
<MediaAttachmentGrid attachments={toot().mediaAttachments} />
|
<MediaAttachmentGrid attachments={toot().mediaAttachments} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .toot {
|
&>.toot {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
|
|
||||||
> * {
|
>* {
|
||||||
color: var(--tutu-color-secondary-text-on-surface);
|
color: var(--tutu-color-secondary-text-on-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
grid-column: 1 /3;
|
grid-column: 1 /3;
|
||||||
}
|
}
|
||||||
|
|
||||||
> time {
|
>time {
|
||||||
text-align: end;
|
text-align: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +104,64 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact {
|
.previewCard {
|
||||||
|
composes: cardGutSkip from "../material/cards.module.css";
|
||||||
|
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;
|
||||||
|
|
||||||
|
>img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&: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);
|
||||||
|
}
|
||||||
|
|
||||||
|
>h1,
|
||||||
|
>p {
|
||||||
|
margin-left: 16px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(10%, 30%) 1fr;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
|
||||||
|
>img:first-child {
|
||||||
|
grid-row: 1 / 3;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
>h1,
|
||||||
|
>p {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toot.compact {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
@ -127,12 +184,12 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
> .compactAuthorUsername {
|
>.compactAuthorUsername {
|
||||||
color: var(--tutu-color-secondary-text-on-surface);
|
color: var(--tutu-color-secondary-text-on-surface);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
> time {
|
>time {
|
||||||
color: var(--tutu-color-secondary-text-on-surface);
|
color: var(--tutu-color-secondary-text-on-surface);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,11 +234,11 @@
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
|
|
||||||
> button {
|
>button {
|
||||||
color: var(--tutu-color-on-surface);
|
color: var(--tutu-color-on-surface);
|
||||||
padding: 10px 8px;
|
padding: 10px 8px;
|
||||||
|
|
||||||
> svg {
|
>svg {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -207,4 +264,4 @@
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue