Compare commits
2 commits
63fe4acc98
...
6879eb5292
Author | SHA1 | Date | |
---|---|---|---|
|
6879eb5292 | ||
|
1641f3e75b |
3 changed files with 119 additions and 26 deletions
|
@ -24,12 +24,8 @@ dialog.StackedPage {
|
||||||
background: var(--tutu-color-surface);
|
background: var(--tutu-color-surface);
|
||||||
box-shadow: var(--tutu-shadow-e16);
|
box-shadow: var(--tutu-shadow-e16);
|
||||||
|
|
||||||
@media (min-width: 560px) {
|
margin-left: auto;
|
||||||
& {
|
margin-right: auto;
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
& {
|
& {
|
||||||
|
@ -50,4 +46,12 @@ dialog.StackedPage {
|
||||||
&::backdrop {
|
&::backdrop {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.animating {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
* {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -6,6 +6,7 @@ import {
|
||||||
createRenderEffect,
|
createRenderEffect,
|
||||||
createUniqueId,
|
createUniqueId,
|
||||||
Index,
|
Index,
|
||||||
|
onMount,
|
||||||
Show,
|
Show,
|
||||||
untrack,
|
untrack,
|
||||||
useContext,
|
useContext,
|
||||||
|
@ -15,9 +16,7 @@ import { createStore, unwrap } from "solid-js/store";
|
||||||
import "./StackedRouter.css";
|
import "./StackedRouter.css";
|
||||||
import { animateSlideInFromRight, animateSlideOutToRight } from "./anim";
|
import { animateSlideInFromRight, animateSlideOutToRight } from "./anim";
|
||||||
import { ANIM_CURVE_DECELERATION, ANIM_CURVE_STD } from "../material/theme";
|
import { ANIM_CURVE_DECELERATION, ANIM_CURVE_STD } from "../material/theme";
|
||||||
import {
|
import { makeEventListener } from "@solid-primitives/event-listener";
|
||||||
makeEventListener,
|
|
||||||
} from "@solid-primitives/event-listener";
|
|
||||||
|
|
||||||
export type StackedRouterProps = Omit<RouterProps, "url">;
|
export type StackedRouterProps = Omit<RouterProps, "url">;
|
||||||
|
|
||||||
|
@ -25,6 +24,9 @@ export type StackFrame = {
|
||||||
path: string;
|
path: string;
|
||||||
rootId: string;
|
rootId: string;
|
||||||
state: unknown;
|
state: unknown;
|
||||||
|
|
||||||
|
animateOpen?: (element: HTMLElement) => Animation;
|
||||||
|
animateClose?: (element: HTMLElement) => Animation;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NewFrameOptions<T> = (T extends undefined
|
export type NewFrameOptions<T> = (T extends undefined
|
||||||
|
@ -33,6 +35,8 @@ export type NewFrameOptions<T> = (T extends undefined
|
||||||
}
|
}
|
||||||
: { state: T }) & {
|
: { state: T }) & {
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
|
animateOpen?: StackFrame["animateOpen"];
|
||||||
|
animateClose?: StackFrame["animateClose"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FramePusher<T, K extends keyof T = keyof T> = T[K] extends
|
export type FramePusher<T, K extends keyof T = keyof T> = T[K] extends
|
||||||
|
@ -117,9 +121,11 @@ function animateClose(element: HTMLElement) {
|
||||||
|
|
||||||
function animateOpen(element: HTMLElement) {
|
function animateOpen(element: HTMLElement) {
|
||||||
if (window.innerWidth <= 560) {
|
if (window.innerWidth <= 560) {
|
||||||
animateSlideInFromRight(element, { easing: ANIM_CURVE_DECELERATION });
|
return animateSlideInFromRight(element, {
|
||||||
|
easing: ANIM_CURVE_DECELERATION,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
element.animate(
|
return element.animate(
|
||||||
{
|
{
|
||||||
opacity: [0.5, 1],
|
opacity: [0.5, 1],
|
||||||
},
|
},
|
||||||
|
@ -128,6 +134,17 @@ function animateOpen(element: HTMLElement) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serializableStack(stack: readonly StackFrame[]) {
|
||||||
|
const frames = unwrap(stack);
|
||||||
|
return frames.map((fr) => {
|
||||||
|
return {
|
||||||
|
path: fr.path,
|
||||||
|
rootId: fr.rootId,
|
||||||
|
state: fr.state,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The router that stacks the pages.
|
* The router that stacks the pages.
|
||||||
*/
|
*/
|
||||||
|
@ -140,13 +157,15 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
|
||||||
path,
|
path,
|
||||||
state: opts?.state,
|
state: opts?.state,
|
||||||
rootId: createUniqueId(),
|
rootId: createUniqueId(),
|
||||||
|
animateOpen: opts?.animateOpen,
|
||||||
|
animateClose: opts?.animateClose,
|
||||||
};
|
};
|
||||||
|
|
||||||
mutStack(opts?.replace ? stack.length - 1 : stack.length, frame);
|
mutStack(opts?.replace ? stack.length - 1 : stack.length, frame);
|
||||||
if (opts?.replace) {
|
if (opts?.replace) {
|
||||||
window.history.replaceState(unwrap(stack), "", path);
|
window.history.replaceState(serializableStack(stack), "", path);
|
||||||
} else {
|
} else {
|
||||||
window.history.pushState(unwrap(stack), "", path);
|
window.history.pushState(serializableStack(stack), "", path);
|
||||||
}
|
}
|
||||||
return frame;
|
return frame;
|
||||||
});
|
});
|
||||||
|
@ -165,10 +184,17 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
|
||||||
}
|
}
|
||||||
if (stack.length > 1) {
|
if (stack.length > 1) {
|
||||||
const lastFrame = stack[stack.length - 1];
|
const lastFrame = stack[stack.length - 1];
|
||||||
const element = document.getElementById(lastFrame.rootId)!;
|
const element = document.getElementById(
|
||||||
|
lastFrame.rootId,
|
||||||
|
)! as HTMLDialogElement;
|
||||||
|
const createAnimation = lastFrame.animateClose ?? animateClose;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const animation = animateClose(element);
|
element.classList.add("animating")
|
||||||
animation.addEventListener("finish", () => onlyPopFrame(depth));
|
const animation = createAnimation(element);
|
||||||
|
animation.addEventListener("finish", () => {
|
||||||
|
element.classList.remove("animating")
|
||||||
|
onlyPopFrame(depth);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
onlyPopFrame(depth);
|
onlyPopFrame(depth);
|
||||||
|
@ -195,10 +221,16 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const onBeforeDialogMount = (element: HTMLDialogElement) => {
|
const onBeforeDialogMount = (element: HTMLDialogElement) => {
|
||||||
createEffect(() => {
|
onMount(() => {
|
||||||
|
const lastFr = untrack(() => stack[stack.length - 1]);
|
||||||
|
const createAnimation = lastFr.animateOpen ?? animateOpen;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
element.showModal();
|
element.showModal();
|
||||||
animateOpen(element);
|
element.classList.add("animating");
|
||||||
|
const animation = createAnimation(element);
|
||||||
|
animation.addEventListener("finish", () =>
|
||||||
|
element.classList.remove("animating"),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { useDefaultSession } from "../masto/clients";
|
||||||
import { useHeroSource } from "../platform/anim";
|
import { useHeroSource } from "../platform/anim";
|
||||||
import { HERO as BOTTOM_SHEET_HERO } from "../material/BottomSheet";
|
import { HERO as BOTTOM_SHEET_HERO } from "../material/BottomSheet";
|
||||||
import { setCache as setTootBottomSheetCache } from "./TootBottomSheet";
|
import { setCache as setTootBottomSheetCache } from "./TootBottomSheet";
|
||||||
import { useNavigate } from "@solidjs/router";
|
|
||||||
import RegularToot, {
|
import RegularToot, {
|
||||||
findElementActionable,
|
findElementActionable,
|
||||||
findRootToot,
|
findRootToot,
|
||||||
|
@ -21,6 +20,21 @@ import RegularToot, {
|
||||||
import cardStyle from "../material/cards.module.css";
|
import cardStyle from "../material/cards.module.css";
|
||||||
import type { ThreadNode } from "../masto/timelines";
|
import type { ThreadNode } from "../masto/timelines";
|
||||||
import { useNavigator } from "../platform/StackedRouter";
|
import { useNavigator } from "../platform/StackedRouter";
|
||||||
|
import { ANIM_CURVE_STD } from "../material/theme";
|
||||||
|
|
||||||
|
function durationOf(rect0: DOMRect, rect1: DOMRect) {
|
||||||
|
const distancelt = Math.sqrt(
|
||||||
|
Math.pow(Math.abs(rect0.top - rect1.top), 2) +
|
||||||
|
Math.pow(Math.abs(rect0.left - rect1.left), 2),
|
||||||
|
);
|
||||||
|
const distancerb = Math.sqrt(
|
||||||
|
Math.pow(Math.abs(rect0.bottom - rect1.bottom), 2) +
|
||||||
|
Math.pow(Math.abs(rect0.right - rect1.right), 2),
|
||||||
|
);
|
||||||
|
const distance = distancelt + distancerb;
|
||||||
|
const duration = distance / 1.6;
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
function positionTootInThread(index: number, threadLength: number) {
|
function positionTootInThread(index: number, threadLength: number) {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
|
@ -41,7 +55,7 @@ const TootList: Component<{
|
||||||
const session = useDefaultSession();
|
const session = useDefaultSession();
|
||||||
const heroSrc = useHeroSource();
|
const heroSrc = useHeroSource();
|
||||||
const [expandedThreadId, setExpandedThreadId] = createSignal<string>();
|
const [expandedThreadId, setExpandedThreadId] = createSignal<string>();
|
||||||
const {push} = useNavigator();
|
const { push } = useNavigator();
|
||||||
|
|
||||||
const onBookmark = async (status: mastodon.v1.Status) => {
|
const onBookmark = async (status: mastodon.v1.Status) => {
|
||||||
const client = session()?.client;
|
const client = session()?.client;
|
||||||
|
@ -100,7 +114,7 @@ const TootList: Component<{
|
||||||
|
|
||||||
const openFullScreenToot = (
|
const openFullScreenToot = (
|
||||||
toot: mastodon.v1.Status,
|
toot: mastodon.v1.Status,
|
||||||
srcElement?: HTMLElement,
|
srcElement: HTMLElement,
|
||||||
reply?: boolean,
|
reply?: boolean,
|
||||||
) => {
|
) => {
|
||||||
const p = session()?.account;
|
const p = session()?.account;
|
||||||
|
@ -116,12 +130,55 @@ const TootList: Component<{
|
||||||
|
|
||||||
const acct = `${inf.username}@${p.site}`;
|
const acct = `${inf.username}@${p.site}`;
|
||||||
setTootBottomSheetCache(acct, toot);
|
setTootBottomSheetCache(acct, toot);
|
||||||
|
|
||||||
push(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
|
push(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
|
||||||
state: reply
|
animateOpen(element) {
|
||||||
? {
|
const rect0 = srcElement.getBoundingClientRect(); // the start rect
|
||||||
tootReply: true,
|
const rect1 = element.getBoundingClientRect(); // the end rect
|
||||||
}
|
|
||||||
: undefined,
|
const duration = durationOf(rect0, rect1);
|
||||||
|
|
||||||
|
const keyframes = {
|
||||||
|
top: [`${rect0.top}px`, `${rect1.top}px`],
|
||||||
|
bottom: [`${rect0.bottom}px`, `${rect1.bottom}px`],
|
||||||
|
left: [`${rect0.left}px`, `${rect1.left}px`],
|
||||||
|
right: [`${rect0.right}px`, `${rect1.right}px`],
|
||||||
|
height: [`${rect0.height}px`, `${rect1.height}px`],
|
||||||
|
margin: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
srcElement.style.visibility = "hidden";
|
||||||
|
|
||||||
|
const animation = element.animate(keyframes, {
|
||||||
|
duration,
|
||||||
|
easing: ANIM_CURVE_STD,
|
||||||
|
});
|
||||||
|
return animation;
|
||||||
|
},
|
||||||
|
|
||||||
|
animateClose(element) {
|
||||||
|
const rect0 = element.getBoundingClientRect(); // the start rect
|
||||||
|
const rect1 = srcElement.getBoundingClientRect(); // the end rect
|
||||||
|
|
||||||
|
const duration = durationOf(rect0, rect1);
|
||||||
|
|
||||||
|
const keyframes = {
|
||||||
|
top: [`${rect0.top}px`, `${rect1.top}px`],
|
||||||
|
bottom: [`${rect0.bottom}px`, `${rect1.bottom}px`],
|
||||||
|
left: [`${rect0.left}px`, `${rect1.left}px`],
|
||||||
|
right: [`${rect0.right}px`, `${rect1.right}px`],
|
||||||
|
height: [`${rect0.height}px`, `${rect1.height}px`],
|
||||||
|
margin: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
srcElement.style.visibility = "";
|
||||||
|
|
||||||
|
const animation = element.animate(keyframes, {
|
||||||
|
duration,
|
||||||
|
easing: ANIM_CURVE_STD,
|
||||||
|
});
|
||||||
|
return animation;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue