StackedRouter: new router simulates app behaviour #45
21 changed files with 442 additions and 109 deletions
30
src/App.tsx
30
src/App.tsx
|
@ -27,6 +27,7 @@ import {
|
|||
import { Service } from "./serviceworker/services.js";
|
||||
import { makeEventListener } from "@solid-primitives/event-listener";
|
||||
import { ServiceWorkerProvider } from "./platform/host.js";
|
||||
import StackedRouter from "./platform/StackedRouter.js";
|
||||
|
||||
const AccountSignIn = lazy(() => import("./accounts/SignIn.js"));
|
||||
const AccountMastodonOAuth2Callback = lazy(
|
||||
|
@ -37,24 +38,21 @@ const Settings = lazy(() => import("./settings/Settings.js"));
|
|||
const TootBottomSheet = lazy(() => import("./timelines/TootBottomSheet.js"));
|
||||
const MotionSettings = lazy(() => import("./settings/Motions.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 Profile = lazy(() => import("./profiles/Profile.js"));
|
||||
|
||||
const Routing: Component = () => {
|
||||
return (
|
||||
<Router>
|
||||
<Route path="/" component={TimelineHome}>
|
||||
<Route path=""></Route>
|
||||
<Route path="/settings" component={Settings}>
|
||||
<Route path=""></Route>
|
||||
<Route path="/language" component={LanguageSettings}></Route>
|
||||
<Route path="/region" component={RegionSettings}></Route>
|
||||
<Route path="/motions" component={MotionSettings}></Route>
|
||||
</Route>
|
||||
<Route path="/:acct/toot/:id" component={TootBottomSheet}></Route>
|
||||
<Route path="/:acct/profile/:id" component={Profile}></Route>
|
||||
</Route>
|
||||
<StackedRouter>
|
||||
<Route path="/" component={TimelineHome} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/settings/language" component={LanguageSettings} />
|
||||
<Route path="/settings/region" component={RegionSettings} />
|
||||
<Route path="/motions" component={MotionSettings} />
|
||||
<Route path="/:acct/toot/:id" component={TootBottomSheet} />
|
||||
<Route path="/:acct/profile/:id" component={Profile} />
|
||||
|
||||
<Route path={"/accounts"}>
|
||||
<Route path={"/sign-in"} component={AccountSignIn} />
|
||||
<Route
|
||||
|
@ -62,7 +60,7 @@ const Routing: Component = () => {
|
|||
component={AccountMastodonOAuth2Callback}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</StackedRouter>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -70,7 +68,9 @@ const App: Component = () => {
|
|||
const theme = useRootTheme();
|
||||
const accts = useStore($accounts);
|
||||
const lang = useLanguage();
|
||||
const [serviceWorker, setServiceWorker] = createSignal<ServiceWorker>();
|
||||
const [serviceWorker, setServiceWorker] = createSignal<
|
||||
ServiceWorker | undefined
|
||||
>(undefined, { name: "serviceWorker" });
|
||||
const dispatcher = new ResultDispatcher();
|
||||
|
||||
let checkAge = 0;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useNavigate, useSearchParams } from "@solidjs/router";
|
||||
import { useSearchParams } from "@solidjs/router";
|
||||
import {
|
||||
Component,
|
||||
Show,
|
||||
|
@ -14,6 +14,7 @@ import { LinearProgress } from "@suid/material";
|
|||
import Img from "../material/Img";
|
||||
import { createRestAPIClient } from "masto";
|
||||
import { Title } from "../material/typography";
|
||||
import { useNavigator } from "../platform/StackedRouter";
|
||||
|
||||
type OAuth2CallbackParams = {
|
||||
code?: string;
|
||||
|
@ -25,7 +26,7 @@ const MastodonOAuth2Callback: Component = () => {
|
|||
const progressId = createUniqueId();
|
||||
const titleId = createUniqueId();
|
||||
const [params] = useSearchParams<OAuth2CallbackParams>();
|
||||
const navigate = useNavigate();
|
||||
const { push: navigate } = useNavigator();
|
||||
const setDocumentTitle = useDocumentTitle("Back from Mastodon...");
|
||||
const [siteImg, setSiteImg] = createSignal<{
|
||||
src: string;
|
||||
|
|
|
@ -8,7 +8,8 @@ import {
|
|||
} from "solid-js";
|
||||
import { Account } from "../accounts/stores";
|
||||
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> = {};
|
||||
|
||||
|
@ -56,12 +57,12 @@ export const Provider = Context.Provider;
|
|||
|
||||
export function useSessions() {
|
||||
const sessions = useSessionsRaw();
|
||||
const navigate = useNavigate();
|
||||
const {push} = useNavigator();
|
||||
const location = useLocation();
|
||||
|
||||
createRenderEffect(() => {
|
||||
if (sessions().length > 0) return;
|
||||
navigate(
|
||||
push(
|
||||
"/accounts/sign-in?back=" + encodeURIComponent(location.pathname),
|
||||
{ replace: true },
|
||||
);
|
||||
|
|
|
@ -25,22 +25,6 @@
|
|||
|
||||
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) {
|
||||
& {
|
||||
left: 0;
|
||||
|
|
|
@ -1,22 +1,42 @@
|
|||
|
||||
.Scaffold__topbar {
|
||||
.Scaffold>.topbar {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
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;
|
||||
bottom: 40px;
|
||||
right: 40px;
|
||||
z-index: var(--tutu-zidx-nav, auto);
|
||||
}
|
||||
|
||||
.Scaffold__bottom-dock {
|
||||
.Scaffold>.bottom-dock {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: var(--tutu-zidx-nav, auto);
|
||||
padding-bottom: var(--safe-area-inset-bottom, 0);
|
||||
}
|
||||
|
||||
.Scaffold {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--tutu-color-surface);
|
||||
}
|
|
@ -28,42 +28,48 @@ const Scaffold: Component<ScaffoldProps> = (props) => {
|
|||
"bottom",
|
||||
"children",
|
||||
"ref",
|
||||
"class",
|
||||
]);
|
||||
const [topbarElement, setTopbarElement] = createSignal<HTMLElement>();
|
||||
|
||||
const topbarSize = createElementSize(topbarElement);
|
||||
|
||||
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}>
|
||||
<div class="Scaffold__topbar" ref={setTopbarElement} role="presentation">
|
||||
<div class="topbar" ref={setTopbarElement} role="presentation">
|
||||
{props.topbar}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.fab}>
|
||||
<div class="Scaffold__fab-dock" role="presentation">{props.fab}</div>
|
||||
<div class="fab-dock" role="presentation">
|
||||
{props.fab}
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
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}
|
||||
>
|
||||
{managed.children}
|
||||
</div>
|
||||
{managed.children}
|
||||
|
||||
<Show when={props.bottom}>
|
||||
<div class="Scaffold__bottom-dock" role="presentation">{props.bottom}</div>
|
||||
<div class="bottom-dock" role="presentation">
|
||||
{props.bottom}
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
19
src/platform/A.tsx
Normal file
19
src/platform/A.tsx
Normal 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;
|
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;
|
52
src/platform/StackedRouter.css
Normal file
52
src/platform/StackedRouter.css
Normal 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;
|
||||
}
|
||||
}
|
228
src/platform/StackedRouter.tsx
Normal file
228
src/platform/StackedRouter.tsx
Normal 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;
|
|
@ -1,4 +1,6 @@
|
|||
.Profile {
|
||||
height: 100%;
|
||||
|
||||
.intro {
|
||||
background-color: var(--tutu-color-surface-d);
|
||||
color: var(--tutu-color-on-surface);
|
||||
|
|
|
@ -45,7 +45,7 @@ import {
|
|||
Verified,
|
||||
} from "@suid/icons-material";
|
||||
import { Body2, Title } from "../material/typography";
|
||||
import { useNavigate, useParams } from "@solidjs/router";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { useSessionForAcctStr } from "../masto/clients";
|
||||
import { resolveCustomEmoji } from "../masto/toot";
|
||||
import { FastAverageColor } from "fast-average-color";
|
||||
|
@ -57,9 +57,10 @@ import TootFilterButton from "./TootFilterButton";
|
|||
import Menu, { createManagedMenuState } from "../material/Menu";
|
||||
import { share } from "../platform/share";
|
||||
import "./Profile.css";
|
||||
import { useNavigator } from "../platform/StackedRouter";
|
||||
|
||||
const Profile: Component = () => {
|
||||
const navigate = useNavigate();
|
||||
const { pop } = useNavigator();
|
||||
const params = useParams<{ acct: string; id: string }>();
|
||||
const acctText = () => decodeURIComponent(params.acct);
|
||||
const session = useSessionForAcctStr(acctText);
|
||||
|
@ -209,11 +210,7 @@ const Profile: Component = () => {
|
|||
paddingTop: "var(--safe-area-inset-top)",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={[navigate, -1]}
|
||||
aria-label="Close"
|
||||
>
|
||||
<IconButton color="inherit" onClick={[pop, 1]} aria-label="Close">
|
||||
<Close />
|
||||
</IconButton>
|
||||
<Title
|
||||
|
|
|
@ -24,10 +24,10 @@ import { Title } from "../material/typography";
|
|||
import type { Template } from "@solid-primitives/i18n";
|
||||
import { useStore } from "@nanostores/solid";
|
||||
import { $settings } from "./stores";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { useNavigator } from "../platform/StackedRouter";
|
||||
|
||||
const ChooseLang: Component = () => {
|
||||
const navigate = useNavigate()
|
||||
const { pop } = useNavigator();
|
||||
const [t] = createTranslator(
|
||||
() => import("./i18n/lang-names.json"),
|
||||
(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(() => {
|
||||
return iso639_1.getAllCodes().filter((x) => !["zh", "en"].includes(x));
|
||||
|
@ -48,8 +48,8 @@ const ChooseLang: Component = () => {
|
|||
const matchedLangCode = createMemo(() => autoMatchLangTag());
|
||||
|
||||
const onCodeChange = (code?: string) => {
|
||||
$settings.setKey("language", code)
|
||||
}
|
||||
$settings.setKey("language", code);
|
||||
};
|
||||
|
||||
return (
|
||||
<Scaffold
|
||||
|
@ -59,7 +59,7 @@ const ChooseLang: Component = () => {
|
|||
variant="dense"
|
||||
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
||||
>
|
||||
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
|
||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
<Title>{t("Choose Language")}</Title>
|
||||
|
@ -96,7 +96,10 @@ const ChooseLang: Component = () => {
|
|||
<ListItemText>{t(`lang.${c}`)}</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Radio
|
||||
checked={code() === c || (code() === undefined && matchedLangCode() == c)}
|
||||
checked={
|
||||
code() === c ||
|
||||
(code() === undefined && matchedLangCode() == c)
|
||||
}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
|
|
|
@ -13,14 +13,14 @@ import {
|
|||
Toolbar,
|
||||
} from "@suid/material";
|
||||
import { Title } from "../material/typography";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { ArrowBack } from "@suid/icons-material";
|
||||
import { createTranslator } from "../platform/i18n";
|
||||
import { useStore } from "@nanostores/solid";
|
||||
import { $settings } from "./stores";
|
||||
import { useNavigator } from "../platform/StackedRouter";
|
||||
|
||||
const Motions: Component = () => {
|
||||
const navigate = useNavigate();
|
||||
const {pop} = useNavigator();
|
||||
const [t] = createTranslator(
|
||||
(code) =>
|
||||
import(`./i18n/${code}.json`) as Promise<{
|
||||
|
@ -36,7 +36,7 @@ const Motions: Component = () => {
|
|||
variant="dense"
|
||||
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
||||
>
|
||||
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
|
||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
<Title>{t("motions")}</Title>
|
||||
|
|
|
@ -20,12 +20,12 @@ import {
|
|||
} from "../platform/i18n";
|
||||
import { Title } from "../material/typography";
|
||||
import type { Template } from "@solid-primitives/i18n";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { $settings } from "./stores";
|
||||
import { useStore } from "@nanostores/solid";
|
||||
import { useNavigator } from "../platform/StackedRouter";
|
||||
|
||||
const ChooseRegion: Component = () => {
|
||||
const navigate = useNavigate();
|
||||
const {pop} = useNavigator();
|
||||
const [t] = createTranslator(
|
||||
() => import("./i18n/lang-names.json"),
|
||||
(code) =>
|
||||
|
@ -54,7 +54,7 @@ const ChooseRegion: Component = () => {
|
|||
variant="dense"
|
||||
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
||||
>
|
||||
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
|
||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
<Title>{t("Choose Region")}</Title>
|
||||
|
|
|
@ -30,7 +30,7 @@ import {
|
|||
Refresh as RefreshIcon,
|
||||
Translate as TranslateIcon,
|
||||
} from "@suid/icons-material";
|
||||
import { A, useNavigate } from "@solidjs/router";
|
||||
import A from "../platform/A.js";
|
||||
import { Title } from "../material/typography.jsx";
|
||||
import { css } from "solid-styled";
|
||||
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 { useServiceWorker } from "../platform/host.js";
|
||||
import { useSessions } from "../masto/clients.js";
|
||||
import { useNavigator } from "../platform/StackedRouter.jsx";
|
||||
|
||||
type Inset = {
|
||||
top?: number;
|
||||
|
@ -170,7 +171,7 @@ const Settings: ParentComponent = (props) => {
|
|||
}>,
|
||||
() => import(`./i18n/lang-names.json`),
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
const {pop} = useNavigator();
|
||||
const settings$ = useStore($settings);
|
||||
const { needRefresh, offlineReady } = useServiceWorker();
|
||||
const dateFnLocale = useDateFnLocale();
|
||||
|
@ -200,7 +201,7 @@ const Settings: ParentComponent = (props) => {
|
|||
variant="dense"
|
||||
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
||||
>
|
||||
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
|
||||
<IconButton color="inherit" onClick={[pop, 11]} disableRipple>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Title>{t("Settings")}</Title>
|
||||
|
@ -208,7 +209,7 @@ const Settings: ParentComponent = (props) => {
|
|||
</AppBar>
|
||||
}
|
||||
>
|
||||
<BottomSheet open={!!subpage()} onClose={() => navigate(-1)}>
|
||||
<BottomSheet open={!!subpage()} onClose={() => pop(1)}>
|
||||
{subpage()}
|
||||
</BottomSheet>
|
||||
|
||||
|
|
|
@ -29,11 +29,11 @@ import BottomSheet, {
|
|||
import { $settings } from "../settings/stores";
|
||||
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 TimelinePanel from "./TimelinePanel";
|
||||
import { useSessions } from "../masto/clients";
|
||||
import { useNavigator } from "../platform/StackedRouter";
|
||||
|
||||
const Home: ParentComponent = (props) => {
|
||||
let panelList: HTMLDivElement;
|
||||
|
@ -53,7 +53,7 @@ const Home: ParentComponent = (props) => {
|
|||
const all = profiles();
|
||||
return all?.[0]?.client;
|
||||
};
|
||||
const navigate = useNavigate();
|
||||
const {pop, push} = useNavigator();
|
||||
|
||||
const [heroSrc, setHeroSrc] = createSignal<HeroSource>({});
|
||||
const [panelOffset, setPanelOffset] = createSignal(0);
|
||||
|
@ -151,7 +151,7 @@ const Home: ParentComponent = (props) => {
|
|||
);
|
||||
const acct = `${inf.username}@${p.account.site}`;
|
||||
setTootBottomSheetCache(acct, toot);
|
||||
navigate(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
|
||||
push(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
|
||||
state: reply
|
||||
? {
|
||||
tootReply: true,
|
||||
|
@ -276,7 +276,7 @@ const Home: ParentComponent = (props) => {
|
|||
</Show>
|
||||
</TimeSourceProvider>
|
||||
<Suspense>
|
||||
<BottomSheet open={!!child()} onClose={() => navigate(-1)}>
|
||||
<BottomSheet open={!!child()} onClose={() => pop(1)}>
|
||||
{child()}
|
||||
</BottomSheet>
|
||||
</Suspense>
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
Star as LikeIcon,
|
||||
FeaturedPlayList as ListIcon,
|
||||
} from "@suid/icons-material";
|
||||
import { A } from "@solidjs/router";
|
||||
import A from "../platform/A";
|
||||
|
||||
const ProfileMenuButton: ParentComponent<{
|
||||
profile?: {
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
.TootBottomSheet {
|
||||
overflow: hidden;
|
||||
height: calc(100% - var(--scaffold-topbar-height, 0px));
|
||||
|
||||
.Scrollable {
|
||||
padding-bottom: var(--safe-area-inset-bottom, 0);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior-y: contain;
|
||||
height: 100%;
|
||||
height: calc(100% - var(--scaffold-topbar-height, 0px));
|
||||
}
|
||||
|
||||
.progress-line {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { useLocation, useNavigate, useParams } from "@solidjs/router";
|
||||
import { useLocation, useParams } from "@solidjs/router";
|
||||
import {
|
||||
catchError,
|
||||
createEffect,
|
||||
createRenderEffect,
|
||||
createResource,
|
||||
createSignal,
|
||||
Show,
|
||||
type Component,
|
||||
} from "solid-js";
|
||||
|
@ -25,6 +24,8 @@ import { useDocumentTitle } from "../utils";
|
|||
import { createTimelineControlsForArray } from "../masto/timelines";
|
||||
import TootList from "./TootList";
|
||||
import "./TootBottomSheet.css";
|
||||
import { useNavigator } from "../platform/StackedRouter";
|
||||
import BackButton from "../platform/BackButton";
|
||||
|
||||
let cachedEntry: [string, mastodon.v1.Status] | undefined;
|
||||
|
||||
|
@ -43,7 +44,7 @@ const TootBottomSheet: Component = (props) => {
|
|||
const location = useLocation<{
|
||||
tootReply?: boolean;
|
||||
}>();
|
||||
const navigate = useNavigate();
|
||||
const { pop, push } = useNavigator();
|
||||
const time = createTimeSource();
|
||||
const acctText = () => decodeURIComponent(params.acct);
|
||||
const session = useSessionForAcctStr(acctText);
|
||||
|
@ -186,7 +187,7 @@ const TootBottomSheet: Component = (props) => {
|
|||
target.dataset.client || `@${new URL(target.href).origin}`,
|
||||
);
|
||||
|
||||
navigate(`/${acct}/profile/${target.dataset.acctId}`);
|
||||
push(`/${acct}/profile/${target.dataset.acctId}`);
|
||||
|
||||
return;
|
||||
} else {
|
||||
|
@ -228,9 +229,7 @@ const TootBottomSheet: Component = (props) => {
|
|||
variant="dense"
|
||||
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
||||
>
|
||||
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<BackButton color="inherit" />
|
||||
<Title component="div" class="name" use:solid-styled>
|
||||
<span
|
||||
ref={(e: HTMLElement) =>
|
||||
|
@ -246,9 +245,7 @@ const TootBottomSheet: Component = (props) => {
|
|||
}
|
||||
class="TootBottomSheet"
|
||||
>
|
||||
<div
|
||||
class="Scrollable"
|
||||
>
|
||||
<div class="Scrollable">
|
||||
<TimeSourceProvider value={time}>
|
||||
<TootList
|
||||
threads={ancestors.list}
|
||||
|
@ -288,9 +285,7 @@ const TootBottomSheet: Component = (props) => {
|
|||
</Show>
|
||||
|
||||
<Show when={tootContextErrorUncaught.loading}>
|
||||
<div
|
||||
class="progress-line"
|
||||
>
|
||||
<div class="progress-line">
|
||||
<CircularProgress style="width: 1.5em; height: 1.5em;" />
|
||||
</div>
|
||||
</Show>
|
||||
|
|
|
@ -20,6 +20,7 @@ import RegularToot, {
|
|||
} from "./RegularToot";
|
||||
import cardStyle from "../material/cards.module.css";
|
||||
import type { ThreadNode } from "../masto/timelines";
|
||||
import { useNavigator } from "../platform/StackedRouter";
|
||||
|
||||
function positionTootInThread(index: number, threadLength: number) {
|
||||
if (index === 0) {
|
||||
|
@ -40,7 +41,7 @@ const TootList: Component<{
|
|||
const session = useDefaultSession();
|
||||
const heroSrc = useHeroSource();
|
||||
const [expandedThreadId, setExpandedThreadId] = createSignal<string>();
|
||||
const navigate = useNavigate();
|
||||
const {push} = useNavigator();
|
||||
|
||||
const onBookmark = async (status: mastodon.v1.Status) => {
|
||||
const client = session()?.client;
|
||||
|
@ -115,7 +116,7 @@ const TootList: Component<{
|
|||
|
||||
const acct = `${inf.username}@${p.site}`;
|
||||
setTootBottomSheetCache(acct, toot);
|
||||
navigate(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
|
||||
push(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
|
||||
state: reply
|
||||
? {
|
||||
tootReply: true,
|
||||
|
@ -146,7 +147,7 @@ const TootList: Component<{
|
|||
target.dataset.client || `@${new URL(target.href).origin}`,
|
||||
);
|
||||
|
||||
navigate(`/${acct}/profile/${target.dataset.acctId}`);
|
||||
push(`/${acct}/profile/${target.dataset.acctId}`);
|
||||
|
||||
return;
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue