StackedRouter: new router simulates app behaviour #45
25 changed files with 775 additions and 236 deletions
30
src/App.tsx
30
src/App.tsx
|
@ -27,6 +27,7 @@ import {
|
||||||
import { Service } from "./serviceworker/services.js";
|
import { Service } from "./serviceworker/services.js";
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener";
|
import { makeEventListener } from "@solid-primitives/event-listener";
|
||||||
import { ServiceWorkerProvider } from "./platform/host.js";
|
import { ServiceWorkerProvider } from "./platform/host.js";
|
||||||
|
import StackedRouter from "./platform/StackedRouter.js";
|
||||||
|
|
||||||
const AccountSignIn = lazy(() => import("./accounts/SignIn.js"));
|
const AccountSignIn = lazy(() => import("./accounts/SignIn.js"));
|
||||||
const AccountMastodonOAuth2Callback = lazy(
|
const AccountMastodonOAuth2Callback = lazy(
|
||||||
|
@ -37,24 +38,21 @@ const Settings = lazy(() => import("./settings/Settings.js"));
|
||||||
const TootBottomSheet = lazy(() => import("./timelines/TootBottomSheet.js"));
|
const TootBottomSheet = lazy(() => import("./timelines/TootBottomSheet.js"));
|
||||||
const MotionSettings = lazy(() => import("./settings/Motions.js"));
|
const MotionSettings = lazy(() => import("./settings/Motions.js"));
|
||||||
const LanguageSettings = lazy(() => import("./settings/Language.js"));
|
const LanguageSettings = lazy(() => import("./settings/Language.js"));
|
||||||
const RegionSettings = lazy(() => import("./settings/Region.jsx"));
|
const RegionSettings = lazy(() => import("./settings/Region.js"));
|
||||||
const UnexpectedError = lazy(() => import("./UnexpectedError.js"));
|
const UnexpectedError = lazy(() => import("./UnexpectedError.js"));
|
||||||
const Profile = lazy(() => import("./profiles/Profile.js"));
|
const Profile = lazy(() => import("./profiles/Profile.js"));
|
||||||
|
|
||||||
const Routing: Component = () => {
|
const Routing: Component = () => {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<StackedRouter>
|
||||||
<Route path="/" component={TimelineHome}>
|
<Route path="/" component={TimelineHome} />
|
||||||
<Route path=""></Route>
|
<Route path="/settings/language" component={LanguageSettings} />
|
||||||
<Route path="/settings" component={Settings}>
|
<Route path="/settings/region" component={RegionSettings} />
|
||||||
<Route path=""></Route>
|
<Route path="/settings/motions" component={MotionSettings} />
|
||||||
<Route path="/language" component={LanguageSettings}></Route>
|
<Route path="/settings" component={Settings} />
|
||||||
<Route path="/region" component={RegionSettings}></Route>
|
<Route path="/:acct/toot/:id" component={TootBottomSheet} />
|
||||||
<Route path="/motions" component={MotionSettings}></Route>
|
<Route path="/:acct/profile/:id" component={Profile} />
|
||||||
</Route>
|
|
||||||
<Route path="/:acct/toot/:id" component={TootBottomSheet}></Route>
|
|
||||||
<Route path="/:acct/profile/:id" component={Profile}></Route>
|
|
||||||
</Route>
|
|
||||||
<Route path={"/accounts"}>
|
<Route path={"/accounts"}>
|
||||||
<Route path={"/sign-in"} component={AccountSignIn} />
|
<Route path={"/sign-in"} component={AccountSignIn} />
|
||||||
<Route
|
<Route
|
||||||
|
@ -62,7 +60,7 @@ const Routing: Component = () => {
|
||||||
component={AccountMastodonOAuth2Callback}
|
component={AccountMastodonOAuth2Callback}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Router>
|
</StackedRouter>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -70,7 +68,9 @@ const App: Component = () => {
|
||||||
const theme = useRootTheme();
|
const theme = useRootTheme();
|
||||||
const accts = useStore($accounts);
|
const accts = useStore($accounts);
|
||||||
const lang = useLanguage();
|
const lang = useLanguage();
|
||||||
const [serviceWorker, setServiceWorker] = createSignal<ServiceWorker>();
|
const [serviceWorker, setServiceWorker] = createSignal<
|
||||||
|
ServiceWorker | undefined
|
||||||
|
>(undefined, { name: "serviceWorker" });
|
||||||
const dispatcher = new ResultDispatcher();
|
const dispatcher = new ResultDispatcher();
|
||||||
|
|
||||||
let checkAge = 0;
|
let checkAge = 0;
|
||||||
|
|
|
@ -49,11 +49,13 @@ const UnexpectedError: Component<{ error?: any }> = (props) => {
|
||||||
<h1>Oh, it is our fault.</h1>
|
<h1>Oh, it is our fault.</h1>
|
||||||
<p>There is an unexpected error in our app, and it's not your fault.</p>
|
<p>There is an unexpected error in our app, and it's not your fault.</p>
|
||||||
<p>
|
<p>
|
||||||
You can reload to see if this guy is gone. If you meet this guy
|
You can restart the app to see if this guy is gone. If you meet this guy
|
||||||
repeatly, please report to us.
|
repeatly, please report to us.
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={() => window.location.reload()}>Reload</Button>
|
<Button onClick={() => (window.location.replace("/"))}>
|
||||||
|
Restart App
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useNavigate, useSearchParams } from "@solidjs/router";
|
import { useSearchParams } from "@solidjs/router";
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
Show,
|
Show,
|
||||||
|
@ -14,6 +14,7 @@ import { LinearProgress } from "@suid/material";
|
||||||
import Img from "../material/Img";
|
import Img from "../material/Img";
|
||||||
import { createRestAPIClient } from "masto";
|
import { createRestAPIClient } from "masto";
|
||||||
import { Title } from "../material/typography";
|
import { Title } from "../material/typography";
|
||||||
|
import { useNavigator } from "../platform/StackedRouter";
|
||||||
|
|
||||||
type OAuth2CallbackParams = {
|
type OAuth2CallbackParams = {
|
||||||
code?: string;
|
code?: string;
|
||||||
|
@ -25,7 +26,7 @@ const MastodonOAuth2Callback: Component = () => {
|
||||||
const progressId = createUniqueId();
|
const progressId = createUniqueId();
|
||||||
const titleId = createUniqueId();
|
const titleId = createUniqueId();
|
||||||
const [params] = useSearchParams<OAuth2CallbackParams>();
|
const [params] = useSearchParams<OAuth2CallbackParams>();
|
||||||
const navigate = useNavigate();
|
const { push: navigate } = useNavigator();
|
||||||
const setDocumentTitle = useDocumentTitle("Back from Mastodon...");
|
const setDocumentTitle = useDocumentTitle("Back from Mastodon...");
|
||||||
const [siteImg, setSiteImg] = createSignal<{
|
const [siteImg, setSiteImg] = createSignal<{
|
||||||
src: string;
|
src: string;
|
||||||
|
|
|
@ -8,7 +8,8 @@ import {
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { Account } from "../accounts/stores";
|
import { Account } from "../accounts/stores";
|
||||||
import { createRestAPIClient, mastodon } from "masto";
|
import { createRestAPIClient, mastodon } from "masto";
|
||||||
import { useLocation, useNavigate } from "@solidjs/router";
|
import { useLocation } from "@solidjs/router";
|
||||||
|
import { useNavigator } from "../platform/StackedRouter";
|
||||||
|
|
||||||
const restfulCache: Record<string, mastodon.rest.Client> = {};
|
const restfulCache: Record<string, mastodon.rest.Client> = {};
|
||||||
|
|
||||||
|
@ -56,12 +57,12 @@ export const Provider = Context.Provider;
|
||||||
|
|
||||||
export function useSessions() {
|
export function useSessions() {
|
||||||
const sessions = useSessionsRaw();
|
const sessions = useSessionsRaw();
|
||||||
const navigate = useNavigate();
|
const {push} = useNavigator();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
createRenderEffect(() => {
|
createRenderEffect(() => {
|
||||||
if (sessions().length > 0) return;
|
if (sessions().length > 0) return;
|
||||||
navigate(
|
push(
|
||||||
"/accounts/sign-in?back=" + encodeURIComponent(location.pathname),
|
"/accounts/sign-in?back=" + encodeURIComponent(location.pathname),
|
||||||
{ replace: true },
|
{ replace: true },
|
||||||
);
|
);
|
||||||
|
|
|
@ -25,22 +25,6 @@
|
||||||
|
|
||||||
box-shadow: var(--tutu-shadow-e16);
|
box-shadow: var(--tutu-shadow-e16);
|
||||||
|
|
||||||
.MuiToolbar-root {
|
|
||||||
>.MuiButtonBase-root {
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-left: -0.5em;
|
|
||||||
margin-right: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: -0.5em;
|
|
||||||
margin-left: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
& {
|
& {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
@ -1,18 +1,32 @@
|
||||||
|
.Scaffold>.topbar {
|
||||||
.Scaffold__topbar {
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
z-index: var(--tutu-zidx-nav, auto);
|
z-index: var(--tutu-zidx-nav, auto);
|
||||||
|
|
||||||
|
.MuiToolbar-root {
|
||||||
|
>.MuiButtonBase-root {
|
||||||
|
&:first-child {
|
||||||
|
margin-left: -0.5em;
|
||||||
|
margin-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: -0.5em;
|
||||||
|
margin-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Scaffold__fab-dock {
|
.Scaffold>.fab-dock {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 40px;
|
bottom: 40px;
|
||||||
right: 40px;
|
right: 40px;
|
||||||
z-index: var(--tutu-zidx-nav, auto);
|
z-index: var(--tutu-zidx-nav, auto);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Scaffold__bottom-dock {
|
.Scaffold>.bottom-dock {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -20,3 +34,9 @@
|
||||||
z-index: var(--tutu-zidx-nav, auto);
|
z-index: var(--tutu-zidx-nav, auto);
|
||||||
padding-bottom: var(--safe-area-inset-bottom, 0);
|
padding-bottom: var(--safe-area-inset-bottom, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Scaffold {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--tutu-color-surface);
|
||||||
|
}
|
|
@ -28,22 +28,15 @@ const Scaffold: Component<ScaffoldProps> = (props) => {
|
||||||
"bottom",
|
"bottom",
|
||||||
"children",
|
"children",
|
||||||
"ref",
|
"ref",
|
||||||
|
"class",
|
||||||
]);
|
]);
|
||||||
const [topbarElement, setTopbarElement] = createSignal<HTMLElement>();
|
const [topbarElement, setTopbarElement] = createSignal<HTMLElement>();
|
||||||
|
|
||||||
const topbarSize = createElementSize(topbarElement);
|
const topbarSize = createElementSize(topbarElement);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Show when={props.topbar}>
|
|
||||||
<div class="Scaffold__topbar" ref={setTopbarElement} role="presentation">
|
|
||||||
{props.topbar}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={props.fab}>
|
|
||||||
<div class="Scaffold__fab-dock" role="presentation">{props.fab}</div>
|
|
||||||
</Show>
|
|
||||||
<div
|
<div
|
||||||
|
class={`Scaffold ${managed.class || ""}`}
|
||||||
ref={(e) => {
|
ref={(e) => {
|
||||||
createRenderEffect(() => {
|
createRenderEffect(() => {
|
||||||
e.style.setProperty(
|
e.style.setProperty(
|
||||||
|
@ -58,12 +51,25 @@ const Scaffold: Component<ScaffoldProps> = (props) => {
|
||||||
}}
|
}}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{managed.children}
|
<Show when={props.topbar}>
|
||||||
|
<div class="topbar" ref={setTopbarElement} role="presentation">
|
||||||
|
{props.topbar}
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.bottom}>
|
|
||||||
<div class="Scaffold__bottom-dock" role="presentation">{props.bottom}</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
<Show when={props.fab}>
|
||||||
|
<div class="fab-dock" role="presentation">
|
||||||
|
{props.fab}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{managed.children}
|
||||||
|
|
||||||
|
<Show when={props.bottom}>
|
||||||
|
<div class="bottom-dock" role="presentation">
|
||||||
|
{props.bottom}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
21
src/platform/A.tsx
Normal file
21
src/platform/A.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { splitProps, type JSX } from "solid-js";
|
||||||
|
import { useNavigator } from "./StackedRouter";
|
||||||
|
import { useResolvedPath } from "@solidjs/router";
|
||||||
|
|
||||||
|
function handleClick(
|
||||||
|
push: (name: string, state: unknown) => void,
|
||||||
|
event: MouseEvent & { currentTarget: HTMLAnchorElement },
|
||||||
|
) {
|
||||||
|
const target = event.currentTarget;
|
||||||
|
event.preventDefault();
|
||||||
|
push(target.href, { state: target.getAttribute("state") || undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
const A = (oprops: Omit<JSX.HTMLElementTags["a"], "onClick" | "onclick">) => {
|
||||||
|
const [props, rest] = splitProps(oprops, ["href"]);
|
||||||
|
const resolvedPath = useResolvedPath(() => props.href || "#");
|
||||||
|
const { push } = useNavigator();
|
||||||
|
return <a onClick={[handleClick, push]} href={resolvedPath()} {...rest}></a>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default A;
|
24
src/platform/BackButton.tsx
Normal file
24
src/platform/BackButton.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import type { IconButtonProps } from "@suid/material/IconButton";
|
||||||
|
import IconButton from "@suid/material/IconButton";
|
||||||
|
import { Show, type Component } from "solid-js";
|
||||||
|
import { useCurrentFrame, useNavigator } from "./StackedRouter";
|
||||||
|
import { ArrowBack, Close } from "@suid/icons-material";
|
||||||
|
|
||||||
|
export type BackButtonProps = Omit<IconButtonProps, "onClick" | "children">;
|
||||||
|
|
||||||
|
const BackButton: Component<BackButtonProps> = (props) => {
|
||||||
|
const currentFrame = useCurrentFrame();
|
||||||
|
const { pop } = useNavigator();
|
||||||
|
|
||||||
|
const hasPrevSubPage = () => currentFrame().index > 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton onClick={[pop, 1]} {...props}>
|
||||||
|
<Show when={hasPrevSubPage()} fallback={<Close />}>
|
||||||
|
<ArrowBack />
|
||||||
|
</Show>
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BackButton;
|
59
src/platform/StackedRouter.css
Normal file
59
src/platform/StackedRouter.css
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
.StackedPage {
|
||||||
|
container: StackedPage / size;
|
||||||
|
display: contents;
|
||||||
|
max-width: 100vw;
|
||||||
|
max-width: 100dvw;
|
||||||
|
|
||||||
|
contain: layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.StackedPage {
|
||||||
|
border: none;
|
||||||
|
position: fixed;
|
||||||
|
padding: 0;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
width: 560px;
|
||||||
|
max-height: 100vh;
|
||||||
|
max-height: 100dvh;
|
||||||
|
background: none;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
contain: strict;
|
||||||
|
contain-intrinsic-size: auto 560px auto 100vh;
|
||||||
|
contain-intrinsic-size: auto 560px auto 100dvh;
|
||||||
|
content-visibility: auto;
|
||||||
|
|
||||||
|
background: var(--tutu-color-surface);
|
||||||
|
box-shadow: var(--tutu-shadow-e16);
|
||||||
|
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
& {
|
||||||
|
width: 100vw;
|
||||||
|
width: 100dvw;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
contain-intrinsic-size: 100vw 100vh;
|
||||||
|
contain-intrinsic-size: 100dvw 100dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::backdrop {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.animating {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
* {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
432
src/platform/StackedRouter.tsx
Normal file
432
src/platform/StackedRouter.tsx
Normal file
|
@ -0,0 +1,432 @@
|
||||||
|
import { StaticRouter, type RouterProps } from "@solidjs/router";
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
createContext,
|
||||||
|
createRenderEffect,
|
||||||
|
createUniqueId,
|
||||||
|
Index,
|
||||||
|
onMount,
|
||||||
|
Show,
|
||||||
|
untrack,
|
||||||
|
useContext,
|
||||||
|
type Accessor,
|
||||||
|
} from "solid-js";
|
||||||
|
import { createStore, unwrap } from "solid-js/store";
|
||||||
|
import "./StackedRouter.css";
|
||||||
|
import { animateSlideInFromRight, animateSlideOutToRight } from "./anim";
|
||||||
|
import { ANIM_CURVE_DECELERATION, ANIM_CURVE_STD } from "../material/theme";
|
||||||
|
import {
|
||||||
|
makeEventListener,
|
||||||
|
} from "@solid-primitives/event-listener";
|
||||||
|
|
||||||
|
export type StackedRouterProps = Omit<RouterProps, "url">;
|
||||||
|
|
||||||
|
export type StackFrame = {
|
||||||
|
path: string;
|
||||||
|
rootId: string;
|
||||||
|
state: unknown;
|
||||||
|
|
||||||
|
animateOpen?: (element: HTMLElement) => Animation;
|
||||||
|
animateClose?: (element: HTMLElement) => Animation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NewFrameOptions<T> = (T extends undefined
|
||||||
|
? {
|
||||||
|
state?: T;
|
||||||
|
}
|
||||||
|
: { state: T }) & {
|
||||||
|
/**
|
||||||
|
* The new frame should replace the current frame.
|
||||||
|
*/
|
||||||
|
replace?: boolean;
|
||||||
|
/**
|
||||||
|
* The animatedOpen phase of the life cycle.
|
||||||
|
*
|
||||||
|
* You can use this hook to animate the opening
|
||||||
|
* of the frame. In this phase, the frame content is created
|
||||||
|
* and is mounted to the document.
|
||||||
|
*
|
||||||
|
* You must return an {@link Animation}. This function must be
|
||||||
|
* without side effects. This phase is ended after the {@link Animation}
|
||||||
|
* finished.
|
||||||
|
*/
|
||||||
|
animateOpen?: StackFrame["animateOpen"];
|
||||||
|
/**
|
||||||
|
* The animatedClose phase of the life cycle.
|
||||||
|
*
|
||||||
|
* You can use this hook to animate the closing of the frame.
|
||||||
|
* In this phase, the frame content is still mounted in the
|
||||||
|
* document and will be unmounted after this phase.
|
||||||
|
*
|
||||||
|
* You must return an {@link Animation}. This function must be
|
||||||
|
* without side effects. This phase is ended after the
|
||||||
|
* {@link Animation} finished.
|
||||||
|
*/
|
||||||
|
animateClose?: StackFrame["animateClose"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FramePusher<T, K extends keyof T = keyof T> = T[K] extends
|
||||||
|
| undefined
|
||||||
|
| any
|
||||||
|
? (path: K, state?: Readonly<NewFrameOptions<T[K]>>) => Readonly<StackFrame>
|
||||||
|
: (path: K, state: Readonly<NewFrameOptions<T[K]>>) => Readonly<StackFrame>;
|
||||||
|
|
||||||
|
export type Navigator<PushGuide = Record<string, any>> = {
|
||||||
|
frames: readonly StackFrame[];
|
||||||
|
push: FramePusher<PushGuide>;
|
||||||
|
pop: (depth?: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NavigatorContext = /* @__PURE__ */ createContext<Navigator>();
|
||||||
|
|
||||||
|
export function useMaybeNavigator() {
|
||||||
|
return useContext(NavigatorContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the navigator of the {@link StackedRouter}.
|
||||||
|
*
|
||||||
|
* This function returns a {@link Navigator} without available
|
||||||
|
* push guide. Push guide is a record type contains available
|
||||||
|
* path and its state. If you need push guide, you may want to
|
||||||
|
* define your own function (like `useAppNavigator`) and cast the
|
||||||
|
* navigator to the type you need.
|
||||||
|
*/
|
||||||
|
export function useNavigator() {
|
||||||
|
const navigator = useMaybeNavigator();
|
||||||
|
|
||||||
|
if (!navigator) {
|
||||||
|
throw new TypeError("not in available scope of StackedRouter");
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CurrentFrame = {
|
||||||
|
index: number;
|
||||||
|
frame: Readonly<StackFrame>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CurrentFrameContext =
|
||||||
|
/* @__PURE__ */ createContext<Accessor<Readonly<CurrentFrame>>>();
|
||||||
|
|
||||||
|
export function useMaybeCurrentFrame() {
|
||||||
|
return useContext(CurrentFrameContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCurrentFrame() {
|
||||||
|
const frame = useMaybeCurrentFrame();
|
||||||
|
|
||||||
|
if (!frame) {
|
||||||
|
throw new TypeError("not in available scope of StackedRouter");
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an accessor of is current frame is suspended.
|
||||||
|
*
|
||||||
|
* A suspended frame is the one not on the top. "Suspended"
|
||||||
|
* is the description of a certain situtation, not in the life cycle
|
||||||
|
* of a frame.
|
||||||
|
*/
|
||||||
|
export function useMaybeIsFrameSuspended() {
|
||||||
|
const { frames } = useMaybeNavigator() || {};
|
||||||
|
|
||||||
|
if (typeof frames === "undefined") {
|
||||||
|
return () => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thisFrame = useCurrentFrame();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const idx = thisFrame().index;
|
||||||
|
return frames.length - 1 > idx;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDialogClick(
|
||||||
|
onClose: () => void,
|
||||||
|
event: MouseEvent & { currentTarget: HTMLDialogElement },
|
||||||
|
) {
|
||||||
|
if (event.target !== event.currentTarget) return;
|
||||||
|
const rect = event.currentTarget.getBoundingClientRect();
|
||||||
|
const isNotInDialog =
|
||||||
|
event.clientY < rect.top ||
|
||||||
|
event.clientY > rect.bottom ||
|
||||||
|
event.clientX < rect.left ||
|
||||||
|
event.clientX > rect.right;
|
||||||
|
if (isNotInDialog) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateClose(element: HTMLElement) {
|
||||||
|
if (window.innerWidth <= 560) {
|
||||||
|
return animateSlideOutToRight(element, { easing: ANIM_CURVE_DECELERATION });
|
||||||
|
} else {
|
||||||
|
return element.animate(
|
||||||
|
{
|
||||||
|
opacity: [0.5, 0],
|
||||||
|
},
|
||||||
|
{ easing: ANIM_CURVE_STD, duration: 220 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateOpen(element: HTMLElement) {
|
||||||
|
if (window.innerWidth <= 560) {
|
||||||
|
return animateSlideInFromRight(element, {
|
||||||
|
easing: ANIM_CURVE_DECELERATION,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return element.animate(
|
||||||
|
{
|
||||||
|
opacity: [0.5, 1],
|
||||||
|
},
|
||||||
|
{ easing: ANIM_CURVE_STD, duration: 220 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializableStack(stack: readonly StackFrame[]) {
|
||||||
|
const frames = unwrap(stack);
|
||||||
|
return frames.map((fr) => {
|
||||||
|
return fr.animateClose || fr.animateOpen
|
||||||
|
? {
|
||||||
|
path: fr.path,
|
||||||
|
rootId: fr.rootId,
|
||||||
|
state: fr.state,
|
||||||
|
}
|
||||||
|
: fr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The router that stacks the pages.
|
||||||
|
*/
|
||||||
|
const StackedRouter: Component<StackedRouterProps> = (oprops) => {
|
||||||
|
const [stack, mutStack] = createStore([] as StackFrame[], { name: "stack" });
|
||||||
|
|
||||||
|
const pushFrame = (path: string, opts?: Readonly<NewFrameOptions<any>>) =>
|
||||||
|
untrack(() => {
|
||||||
|
const frame = {
|
||||||
|
path,
|
||||||
|
state: opts?.state,
|
||||||
|
rootId: createUniqueId(),
|
||||||
|
animateOpen: opts?.animateOpen,
|
||||||
|
animateClose: opts?.animateClose,
|
||||||
|
};
|
||||||
|
|
||||||
|
mutStack(opts?.replace ? stack.length - 1 : stack.length, frame);
|
||||||
|
if (opts?.replace) {
|
||||||
|
window.history.replaceState(serializableStack(stack), "", path);
|
||||||
|
} else {
|
||||||
|
window.history.pushState(serializableStack(stack), "", path);
|
||||||
|
}
|
||||||
|
return frame;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onlyPopFrame = (depth: number) => {
|
||||||
|
mutStack((o) => o.toSpliced(o.length - depth, depth));
|
||||||
|
window.history.go(-depth);
|
||||||
|
};
|
||||||
|
|
||||||
|
const popFrame = (depth: number = 1) =>
|
||||||
|
untrack(() => {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
if (depth < 0) {
|
||||||
|
console.warn("the depth to pop should not < 0, now is", depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stack.length > 1) {
|
||||||
|
const lastFrame = stack[stack.length - 1];
|
||||||
|
const element = document.getElementById(
|
||||||
|
lastFrame.rootId,
|
||||||
|
)! as HTMLDialogElement;
|
||||||
|
const createAnimation = lastFrame.animateClose ?? animateClose;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
element.classList.add("animating");
|
||||||
|
const animation = createAnimation(element);
|
||||||
|
animation.addEventListener("finish", () => {
|
||||||
|
element.classList.remove("animating");
|
||||||
|
onlyPopFrame(depth);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onlyPopFrame(depth);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createRenderEffect(() => {
|
||||||
|
if (stack.length === 0) {
|
||||||
|
pushFrame(window.location.pathname);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createRenderEffect(() => {
|
||||||
|
makeEventListener(window, "popstate", (event) => {
|
||||||
|
if (!event.state) return;
|
||||||
|
|
||||||
|
if (stack.length === 0) {
|
||||||
|
mutStack(event.state);
|
||||||
|
} else if (stack.length > event.state.length) {
|
||||||
|
popFrame(stack.length - event.state.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const onBeforeDialogMount = (element: HTMLDialogElement) => {
|
||||||
|
onMount(() => {
|
||||||
|
const lastFr = untrack(() => stack[stack.length - 1]);
|
||||||
|
const createAnimation = lastFr.animateOpen ?? animateOpen;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
element.showModal();
|
||||||
|
element.classList.add("animating");
|
||||||
|
const animation = createAnimation(element);
|
||||||
|
animation.addEventListener("finish", () =>
|
||||||
|
element.classList.remove("animating"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let reenterableAnimation: Animation | undefined;
|
||||||
|
let origX = 0,
|
||||||
|
origWidth = 0;
|
||||||
|
|
||||||
|
const resetAnimation = () => {
|
||||||
|
reenterableAnimation = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDialogTouchStart = (
|
||||||
|
event: TouchEvent & { currentTarget: HTMLDialogElement },
|
||||||
|
) => {
|
||||||
|
if (event.touches.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [fig0] = event.touches;
|
||||||
|
const { x, width } = event.currentTarget.getBoundingClientRect();
|
||||||
|
if (fig0.clientX < x - 22 || fig0.clientX > x + 22) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
origX = x;
|
||||||
|
origWidth = width;
|
||||||
|
|
||||||
|
const lastFr = stack[stack.length - 1];
|
||||||
|
const createAnimation = lastFr.animateClose ?? animateClose;
|
||||||
|
reenterableAnimation = createAnimation(event.currentTarget);
|
||||||
|
reenterableAnimation.pause();
|
||||||
|
reenterableAnimation.addEventListener("finish", resetAnimation);
|
||||||
|
reenterableAnimation.addEventListener("cancel", resetAnimation);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDialogTouchMove = (
|
||||||
|
event: TouchEvent & { currentTarget: HTMLDialogElement },
|
||||||
|
) => {
|
||||||
|
if (event.touches.length !== 1) {
|
||||||
|
if (reenterableAnimation) {
|
||||||
|
reenterableAnimation.reverse();
|
||||||
|
reenterableAnimation.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reenterableAnimation) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const [fig0] = event.touches;
|
||||||
|
const ofsX = fig0.clientX - origX;
|
||||||
|
const pc = ofsX / origWidth / window.devicePixelRatio;
|
||||||
|
|
||||||
|
const { activeDuration, delay } =
|
||||||
|
reenterableAnimation.effect!.getComputedTiming();
|
||||||
|
|
||||||
|
const totalTime = (delay || 0) + Number(activeDuration);
|
||||||
|
reenterableAnimation.currentTime = totalTime * pc;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDialogTouchEnd = (event: TouchEvent) => {
|
||||||
|
if (!reenterableAnimation) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const { activeDuration, delay } =
|
||||||
|
reenterableAnimation.effect!.getComputedTiming();
|
||||||
|
const totalTime = (delay || 0) + Number(activeDuration);
|
||||||
|
|
||||||
|
if (Number(reenterableAnimation.currentTime) / totalTime > 0.1) {
|
||||||
|
reenterableAnimation.addEventListener("finish", () => {
|
||||||
|
onlyPopFrame(1);
|
||||||
|
});
|
||||||
|
reenterableAnimation.play();
|
||||||
|
} else {
|
||||||
|
reenterableAnimation.cancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDialogTouchCancel = (event: TouchEvent) => {
|
||||||
|
if (!reenterableAnimation) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
reenterableAnimation.cancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavigatorContext.Provider
|
||||||
|
value={{
|
||||||
|
push: pushFrame,
|
||||||
|
pop: popFrame,
|
||||||
|
frames: stack,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Index each={stack}>
|
||||||
|
{(frame, index) => {
|
||||||
|
const currentFrame = () => {
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
frame: frame(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CurrentFrameContext.Provider value={currentFrame}>
|
||||||
|
<Show
|
||||||
|
when={index !== 0}
|
||||||
|
fallback={
|
||||||
|
<div
|
||||||
|
class="StackedPage"
|
||||||
|
id={frame().rootId}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<StaticRouter url={frame().path} {...oprops} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<dialog
|
||||||
|
ref={onBeforeDialogMount}
|
||||||
|
class="StackedPage"
|
||||||
|
onCancel={[popFrame, 1]}
|
||||||
|
onClick={[onDialogClick, popFrame]}
|
||||||
|
onTouchStart={onDialogTouchStart}
|
||||||
|
onTouchMove={onDialogTouchMove}
|
||||||
|
onTouchEnd={onDialogTouchEnd}
|
||||||
|
onTouchCancel={onDialogTouchCancel}
|
||||||
|
id={frame().rootId}
|
||||||
|
>
|
||||||
|
<StaticRouter url={frame().path} {...oprops} />
|
||||||
|
</dialog>
|
||||||
|
</Show>
|
||||||
|
</CurrentFrameContext.Provider>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Index>
|
||||||
|
</NavigatorContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StackedRouter;
|
|
@ -1,4 +1,6 @@
|
||||||
.Profile {
|
.Profile {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
.intro {
|
.intro {
|
||||||
background-color: var(--tutu-color-surface-d);
|
background-color: var(--tutu-color-surface-d);
|
||||||
color: var(--tutu-color-on-surface);
|
color: var(--tutu-color-on-surface);
|
||||||
|
|
|
@ -45,7 +45,7 @@ import {
|
||||||
Verified,
|
Verified,
|
||||||
} from "@suid/icons-material";
|
} from "@suid/icons-material";
|
||||||
import { Body2, Title } from "../material/typography";
|
import { Body2, Title } from "../material/typography";
|
||||||
import { useNavigate, useParams } from "@solidjs/router";
|
import { useParams } from "@solidjs/router";
|
||||||
import { useSessionForAcctStr } from "../masto/clients";
|
import { useSessionForAcctStr } from "../masto/clients";
|
||||||
import { resolveCustomEmoji } from "../masto/toot";
|
import { resolveCustomEmoji } from "../masto/toot";
|
||||||
import { FastAverageColor } from "fast-average-color";
|
import { FastAverageColor } from "fast-average-color";
|
||||||
|
@ -57,9 +57,10 @@ import TootFilterButton from "./TootFilterButton";
|
||||||
import Menu, { createManagedMenuState } from "../material/Menu";
|
import Menu, { createManagedMenuState } from "../material/Menu";
|
||||||
import { share } from "../platform/share";
|
import { share } from "../platform/share";
|
||||||
import "./Profile.css";
|
import "./Profile.css";
|
||||||
|
import { useNavigator } from "../platform/StackedRouter";
|
||||||
|
|
||||||
const Profile: Component = () => {
|
const Profile: Component = () => {
|
||||||
const navigate = useNavigate();
|
const { pop } = useNavigator();
|
||||||
const params = useParams<{ acct: string; id: string }>();
|
const params = useParams<{ acct: string; id: string }>();
|
||||||
const acctText = () => decodeURIComponent(params.acct);
|
const acctText = () => decodeURIComponent(params.acct);
|
||||||
const session = useSessionForAcctStr(acctText);
|
const session = useSessionForAcctStr(acctText);
|
||||||
|
@ -209,11 +210,7 @@ const Profile: Component = () => {
|
||||||
paddingTop: "var(--safe-area-inset-top)",
|
paddingTop: "var(--safe-area-inset-top)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton color="inherit" onClick={[pop, 1]} aria-label="Close">
|
||||||
color="inherit"
|
|
||||||
onClick={[navigate, -1]}
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
<Close />
|
<Close />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Title
|
<Title
|
||||||
|
|
|
@ -24,10 +24,10 @@ import { Title } from "../material/typography";
|
||||||
import type { Template } from "@solid-primitives/i18n";
|
import type { Template } from "@solid-primitives/i18n";
|
||||||
import { useStore } from "@nanostores/solid";
|
import { useStore } from "@nanostores/solid";
|
||||||
import { $settings } from "./stores";
|
import { $settings } from "./stores";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigator } from "../platform/StackedRouter";
|
||||||
|
|
||||||
const ChooseLang: Component = () => {
|
const ChooseLang: Component = () => {
|
||||||
const navigate = useNavigate()
|
const { pop } = useNavigator();
|
||||||
const [t] = createTranslator(
|
const [t] = createTranslator(
|
||||||
() => import("./i18n/lang-names.json"),
|
() => import("./i18n/lang-names.json"),
|
||||||
(code) =>
|
(code) =>
|
||||||
|
@ -37,9 +37,9 @@ const ChooseLang: Component = () => {
|
||||||
};
|
};
|
||||||
}>,
|
}>,
|
||||||
);
|
);
|
||||||
const settings = useStore($settings)
|
const settings = useStore($settings);
|
||||||
|
|
||||||
const code = () => settings().language
|
const code = () => settings().language;
|
||||||
|
|
||||||
const unsupportedLangCodes = createMemo(() => {
|
const unsupportedLangCodes = createMemo(() => {
|
||||||
return iso639_1.getAllCodes().filter((x) => !["zh", "en"].includes(x));
|
return iso639_1.getAllCodes().filter((x) => !["zh", "en"].includes(x));
|
||||||
|
@ -48,8 +48,8 @@ const ChooseLang: Component = () => {
|
||||||
const matchedLangCode = createMemo(() => autoMatchLangTag());
|
const matchedLangCode = createMemo(() => autoMatchLangTag());
|
||||||
|
|
||||||
const onCodeChange = (code?: string) => {
|
const onCodeChange = (code?: string) => {
|
||||||
$settings.setKey("language", code)
|
$settings.setKey("language", code);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Scaffold
|
<Scaffold
|
||||||
|
@ -59,7 +59,7 @@ const ChooseLang: Component = () => {
|
||||||
variant="dense"
|
variant="dense"
|
||||||
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
||||||
>
|
>
|
||||||
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
|
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||||
<ArrowBack />
|
<ArrowBack />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Title>{t("Choose Language")}</Title>
|
<Title>{t("Choose Language")}</Title>
|
||||||
|
@ -96,7 +96,10 @@ const ChooseLang: Component = () => {
|
||||||
<ListItemText>{t(`lang.${c}`)}</ListItemText>
|
<ListItemText>{t(`lang.${c}`)}</ListItemText>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Radio
|
<Radio
|
||||||
checked={code() === c || (code() === undefined && matchedLangCode() == c)}
|
checked={
|
||||||
|
code() === c ||
|
||||||
|
(code() === undefined && matchedLangCode() == c)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
|
|
|
@ -13,14 +13,14 @@ import {
|
||||||
Toolbar,
|
Toolbar,
|
||||||
} from "@suid/material";
|
} from "@suid/material";
|
||||||
import { Title } from "../material/typography";
|
import { Title } from "../material/typography";
|
||||||
import { useNavigate } from "@solidjs/router";
|
|
||||||
import { ArrowBack } from "@suid/icons-material";
|
import { ArrowBack } from "@suid/icons-material";
|
||||||
import { createTranslator } from "../platform/i18n";
|
import { createTranslator } from "../platform/i18n";
|
||||||
import { useStore } from "@nanostores/solid";
|
import { useStore } from "@nanostores/solid";
|
||||||
import { $settings } from "./stores";
|
import { $settings } from "./stores";
|
||||||
|
import { useNavigator } from "../platform/StackedRouter";
|
||||||
|
|
||||||
const Motions: Component = () => {
|
const Motions: Component = () => {
|
||||||
const navigate = useNavigate();
|
const {pop} = useNavigator();
|
||||||
const [t] = createTranslator(
|
const [t] = createTranslator(
|
||||||
(code) =>
|
(code) =>
|
||||||
import(`./i18n/${code}.json`) as Promise<{
|
import(`./i18n/${code}.json`) as Promise<{
|
||||||
|
@ -36,7 +36,7 @@ const Motions: Component = () => {
|
||||||
variant="dense"
|
variant="dense"
|
||||||
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
||||||
>
|
>
|
||||||
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
|
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||||
<ArrowBack />
|
<ArrowBack />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Title>{t("motions")}</Title>
|
<Title>{t("motions")}</Title>
|
||||||
|
|
|
@ -20,12 +20,12 @@ import {
|
||||||
} from "../platform/i18n";
|
} from "../platform/i18n";
|
||||||
import { Title } from "../material/typography";
|
import { Title } from "../material/typography";
|
||||||
import type { Template } from "@solid-primitives/i18n";
|
import type { Template } from "@solid-primitives/i18n";
|
||||||
import { useNavigate } from "@solidjs/router";
|
|
||||||
import { $settings } from "./stores";
|
import { $settings } from "./stores";
|
||||||
import { useStore } from "@nanostores/solid";
|
import { useStore } from "@nanostores/solid";
|
||||||
|
import { useNavigator } from "../platform/StackedRouter";
|
||||||
|
|
||||||
const ChooseRegion: Component = () => {
|
const ChooseRegion: Component = () => {
|
||||||
const navigate = useNavigate();
|
const {pop} = useNavigator();
|
||||||
const [t] = createTranslator(
|
const [t] = createTranslator(
|
||||||
() => import("./i18n/lang-names.json"),
|
() => import("./i18n/lang-names.json"),
|
||||||
(code) =>
|
(code) =>
|
||||||
|
@ -54,7 +54,7 @@ const ChooseRegion: Component = () => {
|
||||||
variant="dense"
|
variant="dense"
|
||||||
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
||||||
>
|
>
|
||||||
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
|
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||||
<ArrowBack />
|
<ArrowBack />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Title>{t("Choose Region")}</Title>
|
<Title>{t("Choose Region")}</Title>
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import {
|
import {
|
||||||
children,
|
|
||||||
createSignal,
|
|
||||||
For,
|
For,
|
||||||
Show,
|
Show,
|
||||||
type JSX,
|
type Component,
|
||||||
type ParentComponent,
|
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import Scaffold from "../material/Scaffold.js";
|
import Scaffold from "../material/Scaffold.js";
|
||||||
import {
|
import {
|
||||||
|
@ -30,7 +27,7 @@ import {
|
||||||
Refresh as RefreshIcon,
|
Refresh as RefreshIcon,
|
||||||
Translate as TranslateIcon,
|
Translate as TranslateIcon,
|
||||||
} from "@suid/icons-material";
|
} from "@suid/icons-material";
|
||||||
import { A, useNavigate } from "@solidjs/router";
|
import A from "../platform/A.js";
|
||||||
import { Title } from "../material/typography.jsx";
|
import { Title } from "../material/typography.jsx";
|
||||||
import { css } from "solid-styled";
|
import { css } from "solid-styled";
|
||||||
import { signOut, type Account } from "../accounts/stores.js";
|
import { signOut, type Account } from "../accounts/stores.js";
|
||||||
|
@ -44,9 +41,9 @@ import {
|
||||||
useDateFnLocale,
|
useDateFnLocale,
|
||||||
} from "../platform/i18n.jsx";
|
} from "../platform/i18n.jsx";
|
||||||
import { type Template } from "@solid-primitives/i18n";
|
import { type Template } from "@solid-primitives/i18n";
|
||||||
import BottomSheet from "../material/BottomSheet.jsx";
|
|
||||||
import { useServiceWorker } from "../platform/host.js";
|
import { useServiceWorker } from "../platform/host.js";
|
||||||
import { useSessions } from "../masto/clients.js";
|
import { useSessions } from "../masto/clients.js";
|
||||||
|
import { useNavigator } from "../platform/StackedRouter.jsx";
|
||||||
|
|
||||||
type Inset = {
|
type Inset = {
|
||||||
top?: number;
|
top?: number;
|
||||||
|
@ -162,7 +159,7 @@ type Strings = {
|
||||||
["lang.auto"]: Template<{ detected: string }>;
|
["lang.auto"]: Template<{ detected: string }>;
|
||||||
} & Record<string, string | undefined>;
|
} & Record<string, string | undefined>;
|
||||||
|
|
||||||
const Settings: ParentComponent = (props) => {
|
const Settings: Component = () => {
|
||||||
const [t] = createTranslator(
|
const [t] = createTranslator(
|
||||||
(code) =>
|
(code) =>
|
||||||
import(`./i18n/${code}.json`) as Promise<{
|
import(`./i18n/${code}.json`) as Promise<{
|
||||||
|
@ -170,9 +167,9 @@ const Settings: ParentComponent = (props) => {
|
||||||
}>,
|
}>,
|
||||||
() => import(`./i18n/lang-names.json`),
|
() => import(`./i18n/lang-names.json`),
|
||||||
);
|
);
|
||||||
const navigate = useNavigate();
|
const {pop} = useNavigator();
|
||||||
const settings$ = useStore($settings);
|
const settings$ = useStore($settings);
|
||||||
const { needRefresh, offlineReady } = useServiceWorker();
|
const { needRefresh } = useServiceWorker();
|
||||||
const dateFnLocale = useDateFnLocale();
|
const dateFnLocale = useDateFnLocale();
|
||||||
|
|
||||||
const profiles = useSessions();
|
const profiles = useSessions();
|
||||||
|
@ -181,8 +178,6 @@ const Settings: ParentComponent = (props) => {
|
||||||
signOut((a) => a.site === acct.site && a.accessToken === acct.accessToken);
|
signOut((a) => a.site === acct.site && a.accessToken === acct.accessToken);
|
||||||
};
|
};
|
||||||
|
|
||||||
const subpage = children(() => props.children);
|
|
||||||
|
|
||||||
css`
|
css`
|
||||||
ul {
|
ul {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -200,7 +195,7 @@ const Settings: ParentComponent = (props) => {
|
||||||
variant="dense"
|
variant="dense"
|
||||||
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
||||||
>
|
>
|
||||||
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
|
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Title>{t("Settings")}</Title>
|
<Title>{t("Settings")}</Title>
|
||||||
|
@ -208,10 +203,6 @@ const Settings: ParentComponent = (props) => {
|
||||||
</AppBar>
|
</AppBar>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<BottomSheet open={!!subpage()} onClose={() => navigate(-1)}>
|
|
||||||
{subpage()}
|
|
||||||
</BottomSheet>
|
|
||||||
|
|
||||||
<List class="setting-list" use:solid-styled>
|
<List class="setting-list" use:solid-styled>
|
||||||
<li>
|
<li>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
@ -3,11 +3,9 @@ import {
|
||||||
Show,
|
Show,
|
||||||
onMount,
|
onMount,
|
||||||
type ParentComponent,
|
type ParentComponent,
|
||||||
children,
|
createRenderEffect,
|
||||||
Suspense,
|
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { useDocumentTitle } from "../utils";
|
import { useDocumentTitle } from "../utils";
|
||||||
import { type mastodon } from "masto";
|
|
||||||
import Scaffold from "../material/Scaffold";
|
import Scaffold from "../material/Scaffold";
|
||||||
import {
|
import {
|
||||||
AppBar,
|
AppBar,
|
||||||
|
@ -23,14 +21,8 @@ import ProfileMenuButton from "./ProfileMenuButton";
|
||||||
import Tabs from "../material/Tabs";
|
import Tabs from "../material/Tabs";
|
||||||
import Tab from "../material/Tab";
|
import Tab from "../material/Tab";
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener";
|
import { makeEventListener } from "@solid-primitives/event-listener";
|
||||||
import BottomSheet, {
|
|
||||||
HERO as BOTTOM_SHEET_HERO,
|
|
||||||
} from "../material/BottomSheet";
|
|
||||||
import { $settings } from "../settings/stores";
|
import { $settings } from "../settings/stores";
|
||||||
import { useStore } from "@nanostores/solid";
|
import { useStore } from "@nanostores/solid";
|
||||||
import { HeroSourceProvider, type HeroSource } from "../platform/anim";
|
|
||||||
import { useNavigate } from "@solidjs/router";
|
|
||||||
import { setCache as setTootBottomSheetCache } from "./TootBottomSheet";
|
|
||||||
import TrendTimelinePanel from "./TrendTimelinePanel";
|
import TrendTimelinePanel from "./TrendTimelinePanel";
|
||||||
import TimelinePanel from "./TimelinePanel";
|
import TimelinePanel from "./TimelinePanel";
|
||||||
import { useSessions } from "../masto/clients";
|
import { useSessions } from "../masto/clients";
|
||||||
|
@ -43,29 +35,17 @@ const Home: ParentComponent = (props) => {
|
||||||
const settings$ = useStore($settings);
|
const settings$ = useStore($settings);
|
||||||
|
|
||||||
const profiles = useSessions();
|
const profiles = useSessions();
|
||||||
const profile = () => {
|
|
||||||
const all = profiles();
|
|
||||||
if (all.length > 0) {
|
|
||||||
return all[0].account.inf;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const client = () => {
|
const client = () => {
|
||||||
const all = profiles();
|
const all = profiles();
|
||||||
return all?.[0]?.client;
|
return all?.[0]?.client;
|
||||||
};
|
};
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [heroSrc, setHeroSrc] = createSignal<HeroSource>({});
|
|
||||||
const [panelOffset, setPanelOffset] = createSignal(0);
|
|
||||||
const prefetching = () => !settings$().prefetchTootsDisabled;
|
const prefetching = () => !settings$().prefetchTootsDisabled;
|
||||||
const [currentFocusOn, setCurrentFocusOn] = createSignal<HTMLElement[]>([]);
|
|
||||||
const [focusRange, setFocusRange] = createSignal([0, 0] as readonly [
|
const [focusRange, setFocusRange] = createSignal([0, 0] as readonly [
|
||||||
number,
|
number,
|
||||||
number,
|
number,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const child = children(() => props.children);
|
|
||||||
|
|
||||||
let scrollEventLockReleased = true;
|
let scrollEventLockReleased = true;
|
||||||
|
|
||||||
const recalculateTabIndicator = () => {
|
const recalculateTabIndicator = () => {
|
||||||
|
@ -102,17 +82,17 @@ const Home: ParentComponent = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const requestRecalculateTabIndicator = () => {
|
||||||
|
if (scrollEventLockReleased) {
|
||||||
|
requestAnimationFrame(recalculateTabIndicator);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createRenderEffect(() => {
|
||||||
|
makeEventListener(window, "resize", requestRecalculateTabIndicator);
|
||||||
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
makeEventListener(panelList, "scroll", () => {
|
|
||||||
if (scrollEventLockReleased) {
|
|
||||||
requestAnimationFrame(recalculateTabIndicator);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
makeEventListener(window, "resize", () => {
|
|
||||||
if (scrollEventLockReleased) {
|
|
||||||
requestAnimationFrame(recalculateTabIndicator);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
requestAnimationFrame(recalculateTabIndicator);
|
requestAnimationFrame(recalculateTabIndicator);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -135,30 +115,6 @@ const Home: ParentComponent = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openFullScreenToot = (
|
|
||||||
toot: mastodon.v1.Status,
|
|
||||||
srcElement?: HTMLElement,
|
|
||||||
reply?: boolean,
|
|
||||||
) => {
|
|
||||||
const p = profiles()[0];
|
|
||||||
const inf = p.account.inf ?? profile();
|
|
||||||
if (!inf) {
|
|
||||||
console.warn("no account info?");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setHeroSrc((x) =>
|
|
||||||
Object.assign({}, x, { [BOTTOM_SHEET_HERO]: srcElement }),
|
|
||||||
);
|
|
||||||
const acct = `${inf.username}@${p.account.site}`;
|
|
||||||
setTootBottomSheetCache(acct, toot);
|
|
||||||
navigate(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
|
|
||||||
state: reply
|
|
||||||
? {
|
|
||||||
tootReply: true,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
css`
|
css`
|
||||||
.tab-panel {
|
.tab-panel {
|
||||||
|
@ -209,7 +165,7 @@ const Home: ParentComponent = (props) => {
|
||||||
class="responsive"
|
class="responsive"
|
||||||
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
||||||
>
|
>
|
||||||
<Tabs onFocusChanged={setCurrentFocusOn} offset={panelOffset()}>
|
<Tabs>
|
||||||
<Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
|
<Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
|
||||||
Home
|
Home
|
||||||
</Tab>
|
</Tab>
|
||||||
|
@ -239,26 +195,25 @@ const Home: ParentComponent = (props) => {
|
||||||
</AppBar>
|
</AppBar>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<HeroSourceProvider value={[heroSrc, setHeroSrc]}>
|
|
||||||
<TimeSourceProvider value={now}>
|
<TimeSourceProvider value={now}>
|
||||||
<Show when={!!client()}>
|
<Show when={!!client()}>
|
||||||
<div class="panel-list" ref={panelList!}>
|
<div
|
||||||
|
class="panel-list"
|
||||||
|
ref={panelList!}
|
||||||
|
onScroll={requestRecalculateTabIndicator}
|
||||||
|
>
|
||||||
<div class="tab-panel">
|
<div class="tab-panel">
|
||||||
<div>
|
<div>
|
||||||
<TimelinePanel
|
<TimelinePanel
|
||||||
client={client()}
|
client={client()}
|
||||||
name="home"
|
name="home"
|
||||||
prefetch={prefetching()}
|
prefetch={prefetching()}
|
||||||
openFullScreenToot={openFullScreenToot}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-panel">
|
<div class="tab-panel">
|
||||||
<div>
|
<div>
|
||||||
<TrendTimelinePanel
|
<TrendTimelinePanel client={client()} />
|
||||||
client={client()}
|
|
||||||
openFullScreenToot={openFullScreenToot}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-panel">
|
<div class="tab-panel">
|
||||||
|
@ -267,7 +222,6 @@ const Home: ParentComponent = (props) => {
|
||||||
client={client()}
|
client={client()}
|
||||||
name="public"
|
name="public"
|
||||||
prefetch={prefetching()}
|
prefetch={prefetching()}
|
||||||
openFullScreenToot={openFullScreenToot}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -275,12 +229,6 @@ const Home: ParentComponent = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</TimeSourceProvider>
|
</TimeSourceProvider>
|
||||||
<Suspense>
|
|
||||||
<BottomSheet open={!!child()} onClose={() => navigate(-1)}>
|
|
||||||
{child()}
|
|
||||||
</BottomSheet>
|
|
||||||
</Suspense>
|
|
||||||
</HeroSourceProvider>
|
|
||||||
</Scaffold>
|
</Scaffold>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
Star as LikeIcon,
|
Star as LikeIcon,
|
||||||
FeaturedPlayList as ListIcon,
|
FeaturedPlayList as ListIcon,
|
||||||
} from "@suid/icons-material";
|
} from "@suid/icons-material";
|
||||||
import { A } from "@solidjs/router";
|
import A from "../platform/A";
|
||||||
|
|
||||||
const ProfileMenuButton: ParentComponent<{
|
const ProfileMenuButton: ParentComponent<{
|
||||||
profile?: {
|
profile?: {
|
||||||
|
@ -51,7 +51,7 @@ const ProfileMenuButton: ParentComponent<{
|
||||||
props.onClick?.();
|
props.onClick?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const inf = () => props.profile?.account.inf
|
const inf = () => props.profile?.account.inf;
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
props.onClick?.();
|
props.onClick?.();
|
||||||
|
@ -130,7 +130,7 @@ const ProfileMenuButton: ParentComponent<{
|
||||||
{props.children}
|
{props.children}
|
||||||
<Divider />
|
<Divider />
|
||||||
</Show>
|
</Show>
|
||||||
<MenuItem component={A} href="/settings" onClick={onClose}>
|
<MenuItem component={A} href="/settings">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { Refresh as RefreshIcon } from "@suid/icons-material";
|
||||||
import { CircularProgress } from "@suid/material";
|
import { CircularProgress } from "@suid/material";
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener";
|
import { makeEventListener } from "@solid-primitives/event-listener";
|
||||||
import { createVisibilityObserver } from "@solid-primitives/intersection-observer";
|
import { createVisibilityObserver } from "@solid-primitives/intersection-observer";
|
||||||
|
import { useMaybeIsFrameSuspended } from "../platform/StackedRouter";
|
||||||
|
|
||||||
const PullDownToRefresh: Component<{
|
const PullDownToRefresh: Component<{
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
@ -33,6 +34,7 @@ const PullDownToRefresh: Component<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const rootVisible = obvx(() => rootElement);
|
const rootVisible = obvx(() => rootElement);
|
||||||
|
const isFrameSuspended = useMaybeIsFrameSuspended()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!rootVisible()) setPullDown(0);
|
if (!rootVisible()) setPullDown(0);
|
||||||
|
@ -109,6 +111,9 @@ const PullDownToRefresh: Component<{
|
||||||
if (!rootVisible()) {
|
if (!rootVisible()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isFrameSuspended()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const element = props.linkedElement;
|
const element = props.linkedElement;
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
makeEventListener(element, "wheel", handleLinkedWheel);
|
makeEventListener(element, "wheel", handleLinkedWheel);
|
||||||
|
@ -159,6 +164,9 @@ const PullDownToRefresh: Component<{
|
||||||
if (!rootVisible()) {
|
if (!rootVisible()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isFrameSuspended()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const element = props.linkedElement;
|
const element = props.linkedElement;
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
makeEventListener(element, "touchmove", handleTouch);
|
makeEventListener(element, "touchmove", handleTouch);
|
||||||
|
|
|
@ -20,12 +20,6 @@ const TimelinePanel: Component<{
|
||||||
client: mastodon.rest.Client;
|
client: mastodon.rest.Client;
|
||||||
name: "home" | "public";
|
name: "home" | "public";
|
||||||
prefetch?: boolean;
|
prefetch?: boolean;
|
||||||
|
|
||||||
openFullScreenToot: (
|
|
||||||
toot: mastodon.v1.Status,
|
|
||||||
srcElement?: HTMLElement,
|
|
||||||
reply?: boolean,
|
|
||||||
) => void;
|
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>();
|
const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>();
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
.TootBottomSheet {
|
.TootBottomSheet {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: calc(100% - var(--scaffold-topbar-height, 0px));
|
|
||||||
|
|
||||||
.Scrollable {
|
.Scrollable {
|
||||||
padding-bottom: var(--safe-area-inset-bottom, 0);
|
padding-bottom: var(--safe-area-inset-bottom, 0);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overscroll-behavior-y: contain;
|
overscroll-behavior-y: contain;
|
||||||
height: 100%;
|
height: calc(100% - var(--scaffold-topbar-height, 0px));
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-line {
|
.progress-line {
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { useLocation, useNavigate, useParams } from "@solidjs/router";
|
import { useLocation, useParams } from "@solidjs/router";
|
||||||
import {
|
import {
|
||||||
catchError,
|
catchError,
|
||||||
createEffect,
|
createEffect,
|
||||||
createRenderEffect,
|
createRenderEffect,
|
||||||
createResource,
|
createResource,
|
||||||
createSignal,
|
|
||||||
Show,
|
Show,
|
||||||
type Component,
|
type Component,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
|
@ -25,6 +24,8 @@ import { useDocumentTitle } from "../utils";
|
||||||
import { createTimelineControlsForArray } from "../masto/timelines";
|
import { createTimelineControlsForArray } from "../masto/timelines";
|
||||||
import TootList from "./TootList";
|
import TootList from "./TootList";
|
||||||
import "./TootBottomSheet.css";
|
import "./TootBottomSheet.css";
|
||||||
|
import { useNavigator } from "../platform/StackedRouter";
|
||||||
|
import BackButton from "../platform/BackButton";
|
||||||
|
|
||||||
let cachedEntry: [string, mastodon.v1.Status] | undefined;
|
let cachedEntry: [string, mastodon.v1.Status] | undefined;
|
||||||
|
|
||||||
|
@ -43,7 +44,7 @@ const TootBottomSheet: Component = (props) => {
|
||||||
const location = useLocation<{
|
const location = useLocation<{
|
||||||
tootReply?: boolean;
|
tootReply?: boolean;
|
||||||
}>();
|
}>();
|
||||||
const navigate = useNavigate();
|
const { pop, push } = useNavigator();
|
||||||
const time = createTimeSource();
|
const time = createTimeSource();
|
||||||
const acctText = () => decodeURIComponent(params.acct);
|
const acctText = () => decodeURIComponent(params.acct);
|
||||||
const session = useSessionForAcctStr(acctText);
|
const session = useSessionForAcctStr(acctText);
|
||||||
|
@ -186,7 +187,7 @@ const TootBottomSheet: Component = (props) => {
|
||||||
target.dataset.client || `@${new URL(target.href).origin}`,
|
target.dataset.client || `@${new URL(target.href).origin}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
navigate(`/${acct}/profile/${target.dataset.acctId}`);
|
push(`/${acct}/profile/${target.dataset.acctId}`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
@ -228,9 +229,7 @@ const TootBottomSheet: Component = (props) => {
|
||||||
variant="dense"
|
variant="dense"
|
||||||
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
||||||
>
|
>
|
||||||
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
|
<BackButton color="inherit" />
|
||||||
<CloseIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Title component="div" class="name" use:solid-styled>
|
<Title component="div" class="name" use:solid-styled>
|
||||||
<span
|
<span
|
||||||
ref={(e: HTMLElement) =>
|
ref={(e: HTMLElement) =>
|
||||||
|
@ -246,9 +245,7 @@ const TootBottomSheet: Component = (props) => {
|
||||||
}
|
}
|
||||||
class="TootBottomSheet"
|
class="TootBottomSheet"
|
||||||
>
|
>
|
||||||
<div
|
<div class="Scrollable">
|
||||||
class="Scrollable"
|
|
||||||
>
|
|
||||||
<TimeSourceProvider value={time}>
|
<TimeSourceProvider value={time}>
|
||||||
<TootList
|
<TootList
|
||||||
threads={ancestors.list}
|
threads={ancestors.list}
|
||||||
|
@ -288,9 +285,7 @@ const TootBottomSheet: Component = (props) => {
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={tootContextErrorUncaught.loading}>
|
<Show when={tootContextErrorUncaught.loading}>
|
||||||
<div
|
<div class="progress-line">
|
||||||
class="progress-line"
|
|
||||||
>
|
|
||||||
<CircularProgress style="width: 1.5em; height: 1.5em;" />
|
<CircularProgress style="width: 1.5em; height: 1.5em;" />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -13,13 +13,28 @@ 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,
|
||||||
} from "./RegularToot";
|
} from "./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 { 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) {
|
||||||
|
@ -40,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 navigate = useNavigate();
|
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;
|
||||||
|
@ -99,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;
|
||||||
|
@ -115,12 +130,55 @@ const TootList: Component<{
|
||||||
|
|
||||||
const acct = `${inf.username}@${p.site}`;
|
const acct = `${inf.username}@${p.site}`;
|
||||||
setTootBottomSheetCache(acct, toot);
|
setTootBottomSheetCache(acct, toot);
|
||||||
navigate(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
|
|
||||||
state: reply
|
push(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
|
||||||
? {
|
animateOpen(element) {
|
||||||
tootReply: true,
|
const rect0 = srcElement.getBoundingClientRect(); // the start rect
|
||||||
}
|
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;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -146,7 +204,7 @@ const TootList: Component<{
|
||||||
target.dataset.client || `@${new URL(target.href).origin}`,
|
target.dataset.client || `@${new URL(target.href).origin}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
navigate(`/${acct}/profile/${target.dataset.acctId}`);
|
push(`/${acct}/profile/${target.dataset.acctId}`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -13,12 +13,6 @@ import TootList from "./TootList.jsx";
|
||||||
|
|
||||||
const TrendTimelinePanel: Component<{
|
const TrendTimelinePanel: Component<{
|
||||||
client: mastodon.rest.Client;
|
client: mastodon.rest.Client;
|
||||||
|
|
||||||
openFullScreenToot: (
|
|
||||||
toot: mastodon.v1.Status,
|
|
||||||
srcElement?: HTMLElement,
|
|
||||||
reply?: boolean,
|
|
||||||
) => void;
|
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>();
|
const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>();
|
||||||
const [tl, snapshot, { refetch: refetchTimeline }] = createTimelineSnapshot(
|
const [tl, snapshot, { refetch: refetchTimeline }] = createTimelineSnapshot(
|
||||||
|
|
Loading…
Reference in a new issue