first prototype of StackedRouter

This commit is contained in:
thislight 2024-11-16 20:04:55 +08:00
parent 607fa64c05
commit 0710aaf4f3
No known key found for this signature in database
GPG key ID: FCFE5192241CCD4E
21 changed files with 442 additions and 109 deletions

View file

@ -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" component={Settings} />
<Route path="/settings" component={Settings}> <Route path="/settings/language" component={LanguageSettings} />
<Route path=""></Route> <Route path="/settings/region" component={RegionSettings} />
<Route path="/language" component={LanguageSettings}></Route> <Route path="/motions" component={MotionSettings} />
<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;

View file

@ -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;

View file

@ -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 },
); );

View file

@ -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;

View file

@ -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);
}

View file

@ -28,42 +28,48 @@ 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 (
<> <div
class={`Scaffold ${managed.class || ""}`}
ref={(e) => {
createRenderEffect(() => {
e.style.setProperty(
"--scaffold-topbar-height",
(topbarSize.height?.toString() ?? 0) + "px",
);
});
if (managed.ref) {
(managed.ref as (val: typeof e) => void)(e);
}
}}
{...rest}
>
<Show when={props.topbar}> <Show when={props.topbar}>
<div class="Scaffold__topbar" ref={setTopbarElement} role="presentation"> <div class="topbar" ref={setTopbarElement} role="presentation">
{props.topbar} {props.topbar}
</div> </div>
</Show> </Show>
<Show when={props.fab}> <Show when={props.fab}>
<div class="Scaffold__fab-dock" role="presentation">{props.fab}</div> <div class="fab-dock" role="presentation">
{props.fab}
</div>
</Show> </Show>
<div
ref={(e) => {
createRenderEffect(() => {
e.style.setProperty(
"--scaffold-topbar-height",
(topbarSize.height?.toString() ?? 0) + "px",
);
});
if (managed.ref) { {managed.children}
(managed.ref as (val: typeof e) => void)(e);
}
}}
{...rest}
>
{managed.children}
</div>
<Show when={props.bottom}> <Show when={props.bottom}>
<div class="Scaffold__bottom-dock" role="presentation">{props.bottom}</div> <div class="bottom-dock" role="presentation">
{props.bottom}
</div>
</Show> </Show>
</> </div>
); );
}; };

19
src/platform/A.tsx Normal file
View file

@ -0,0 +1,19 @@
import { type JSX } from "solid-js";
import { useNavigator } from "./StackedRouter";
function handleClick(
push: (name: string, state: unknown) => void,
event: MouseEvent & { currentTarget: HTMLAnchorElement },
) {
const target = event.currentTarget;
event.preventDefault();
event.stopPropagation();
push(target.href, { state: target.getAttribute("state") || undefined });
}
const A = (oprops: JSX.HTMLElementTags["a"]) => {
const { push } = useNavigator();
return <a onClick={[handleClick, push]} {...oprops}></a>;
};
export default A;

View 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;

View file

@ -0,0 +1,52 @@
.StackedPage {
container: StackedPage / size;
display: contents;
max-width: 100vw;
max-width: 100dvw;
}
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;
box-shadow: var(--tutu-shadow-e16);
@media (min-width: 560px) {
& {
left: 50%;
transform: translateX(-50%);
}
}
@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;
}
}

View file

@ -0,0 +1,228 @@
import { StaticRouter, type RouterProps } from "@solidjs/router";
import {
Component,
createContext,
createEffect,
createRenderEffect,
createUniqueId,
Index,
onMount,
Show,
untrack,
useContext,
type Accessor,
} from "solid-js";
import { createStore, unwrap } from "solid-js/store";
import { insert, render } from "solid-js/web";
import "./StackedRouter.css";
import { animateSlideInFromRight } from "./anim";
import { ANIM_CURVE_DECELERATION, ANIM_CURVE_STD } from "../material/theme";
export type StackedRouterProps = Omit<RouterProps, "url">;
export type StackFrame = {
path: string;
rootId: string;
state: unknown;
beforeShow?: (element: HTMLElement) => void;
};
export type NewFrameOptions<T> = (T extends undefined
? {
state?: T;
}
: { state: T }) & {
replace?: boolean;
};
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>();
/**
* 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 = useContext(NavigatorContext);
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 useCurrentFrame() {
const frame = useContext(CurrentFrameContext);
if (!frame) {
throw new TypeError("not in available scope of StackedRouter");
}
return frame;
}
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();
}
}
/**
* 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(),
};
mutStack(opts?.replace ? stack.length - 1 : stack.length, frame);
return frame;
});
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)!;
requestAnimationFrame(() => {
const animation = element.animate(
{
opacity: [0.5, 0],
},
{ easing: ANIM_CURVE_STD, duration: 220 },
);
animation.addEventListener("finish", () =>
mutStack((o) => o.toSpliced(o.length - depth, depth)),
);
});
} else {
mutStack((o) => {
return o.toSpliced(o.length - depth, depth);
});
}
});
/* createEffect(() => {
const length = stack.length;
console.debug("stack is changed", length, unwrap(stack));
}); */
createRenderEffect(() => {
if (stack.length === 0) {
pushFrame("/", undefined);
}
});
const onBeforeDialogMount = (element: HTMLDialogElement) => {
createEffect(() => {
requestAnimationFrame(() => {
element.showModal();
if (window.innerWidth <= 560) {
animateSlideInFromRight(element, { easing: ANIM_CURVE_DECELERATION });
} else {
element.animate(
{
opacity: [0.5, 1],
},
{ easing: ANIM_CURVE_STD, duration: 220 },
);
}
});
});
};
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]}
id={frame().rootId}
>
<StaticRouter url={frame().path} {...oprops} />
</dialog>
</Show>
</CurrentFrameContext.Provider>
);
}}
</Index>
</NavigatorContext.Provider>
);
};
export default StackedRouter;

View file

@ -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);

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -30,7 +30,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";
@ -47,6 +47,7 @@ import { type Template } from "@solid-primitives/i18n";
import BottomSheet from "../material/BottomSheet.jsx"; 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;
@ -170,7 +171,7 @@ 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, offlineReady } = useServiceWorker();
const dateFnLocale = useDateFnLocale(); const dateFnLocale = useDateFnLocale();
@ -200,7 +201,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, 11]} disableRipple>
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
<Title>{t("Settings")}</Title> <Title>{t("Settings")}</Title>
@ -208,7 +209,7 @@ const Settings: ParentComponent = (props) => {
</AppBar> </AppBar>
} }
> >
<BottomSheet open={!!subpage()} onClose={() => navigate(-1)}> <BottomSheet open={!!subpage()} onClose={() => pop(1)}>
{subpage()} {subpage()}
</BottomSheet> </BottomSheet>

View file

@ -29,11 +29,11 @@ import 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 { HeroSourceProvider, type HeroSource } from "../platform/anim";
import { useNavigate } from "@solidjs/router";
import { setCache as setTootBottomSheetCache } from "./TootBottomSheet"; 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";
import { useNavigator } from "../platform/StackedRouter";
const Home: ParentComponent = (props) => { const Home: ParentComponent = (props) => {
let panelList: HTMLDivElement; let panelList: HTMLDivElement;
@ -53,7 +53,7 @@ const Home: ParentComponent = (props) => {
const all = profiles(); const all = profiles();
return all?.[0]?.client; return all?.[0]?.client;
}; };
const navigate = useNavigate(); const {pop, push} = useNavigator();
const [heroSrc, setHeroSrc] = createSignal<HeroSource>({}); const [heroSrc, setHeroSrc] = createSignal<HeroSource>({});
const [panelOffset, setPanelOffset] = createSignal(0); const [panelOffset, setPanelOffset] = createSignal(0);
@ -151,7 +151,7 @@ const Home: ParentComponent = (props) => {
); );
const acct = `${inf.username}@${p.account.site}`; const acct = `${inf.username}@${p.account.site}`;
setTootBottomSheetCache(acct, toot); setTootBottomSheetCache(acct, toot);
navigate(`/${encodeURIComponent(acct)}/toot/${toot.id}`, { push(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
state: reply state: reply
? { ? {
tootReply: true, tootReply: true,
@ -276,7 +276,7 @@ const Home: ParentComponent = (props) => {
</Show> </Show>
</TimeSourceProvider> </TimeSourceProvider>
<Suspense> <Suspense>
<BottomSheet open={!!child()} onClose={() => navigate(-1)}> <BottomSheet open={!!child()} onClose={() => pop(1)}>
{child()} {child()}
</BottomSheet> </BottomSheet>
</Suspense> </Suspense>

View file

@ -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?: {

View file

@ -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 {

View file

@ -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>

View file

@ -20,6 +20,7 @@ import RegularToot, {
} 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";
function positionTootInThread(index: number, threadLength: number) { function positionTootInThread(index: number, threadLength: number) {
if (index === 0) { if (index === 0) {
@ -40,7 +41,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;
@ -115,7 +116,7 @@ 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}`, { push(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
state: reply state: reply
? { ? {
tootReply: true, tootReply: true,
@ -146,7 +147,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 {