Compare commits
	
		
			No commits in common. "1a7a52da22414181a4b61007f957c96f3cbbe801" and "607fa64c0596cecc3dfd3f89bb9598e829d341cf" have entirely different histories.
		
	
	
		
			1a7a52da22
			...
			607fa64c05
		
	
		
					 25 changed files with 234 additions and 773 deletions
				
			
		
							
								
								
									
										30
									
								
								src/App.tsx
									
										
									
									
									
								
							
							
						
						
									
										30
									
								
								src/App.tsx
									
										
									
									
									
								
							| 
						 | 
					@ -27,7 +27,6 @@ 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(
 | 
				
			||||||
| 
						 | 
					@ -38,21 +37,24 @@ 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.js"));
 | 
					const RegionSettings = lazy(() => import("./settings/Region.jsx"));
 | 
				
			||||||
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 (
 | 
				
			||||||
    <StackedRouter>
 | 
					    <Router>
 | 
				
			||||||
      <Route path="/" component={TimelineHome} />
 | 
					      <Route path="/" component={TimelineHome}>
 | 
				
			||||||
      <Route path="/settings/language" component={LanguageSettings} />
 | 
					        <Route path=""></Route>
 | 
				
			||||||
      <Route path="/settings/region" component={RegionSettings} />
 | 
					        <Route path="/settings" component={Settings}>
 | 
				
			||||||
      <Route path="/settings/motions" component={MotionSettings} />
 | 
					          <Route path=""></Route>
 | 
				
			||||||
      <Route path="/settings" component={Settings} />
 | 
					          <Route path="/language" component={LanguageSettings}></Route>
 | 
				
			||||||
      <Route path="/:acct/toot/:id" component={TootBottomSheet} />
 | 
					          <Route path="/region" component={RegionSettings}></Route>
 | 
				
			||||||
      <Route path="/:acct/profile/:id" component={Profile} />
 | 
					          <Route path="/motions" component={MotionSettings}></Route>
 | 
				
			||||||
 | 
					        </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
 | 
				
			||||||
| 
						 | 
					@ -60,7 +62,7 @@ const Routing: Component = () => {
 | 
				
			||||||
          component={AccountMastodonOAuth2Callback}
 | 
					          component={AccountMastodonOAuth2Callback}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </Route>
 | 
					      </Route>
 | 
				
			||||||
    </StackedRouter>
 | 
					    </Router>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,9 +70,7 @@ 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<
 | 
					  const [serviceWorker, setServiceWorker] = createSignal<ServiceWorker>();
 | 
				
			||||||
    ServiceWorker | undefined
 | 
					 | 
				
			||||||
  >(undefined, { name: "serviceWorker" });
 | 
					 | 
				
			||||||
  const dispatcher = new ResultDispatcher();
 | 
					  const dispatcher = new ResultDispatcher();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let checkAge = 0;
 | 
					  let checkAge = 0;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,13 +49,11 @@ 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 restart the app to see if this guy is gone. If you meet this guy
 | 
					        You can reload 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.replace("/"))}>
 | 
					        <Button onClick={() => window.location.reload()}>Reload</Button>
 | 
				
			||||||
          Restart App
 | 
					 | 
				
			||||||
        </Button>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <details>
 | 
					      <details>
 | 
				
			||||||
        <summary>
 | 
					        <summary>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { useSearchParams } from "@solidjs/router";
 | 
					import { useNavigate, useSearchParams } from "@solidjs/router";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Component,
 | 
					  Component,
 | 
				
			||||||
  Show,
 | 
					  Show,
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,6 @@ 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;
 | 
				
			||||||
| 
						 | 
					@ -26,7 +25,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 { push: navigate } = useNavigator();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  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,8 +8,7 @@ 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 } from "@solidjs/router";
 | 
					import { useLocation, useNavigate } from "@solidjs/router";
 | 
				
			||||||
import { useNavigator } from "../platform/StackedRouter";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const restfulCache: Record<string, mastodon.rest.Client> = {};
 | 
					const restfulCache: Record<string, mastodon.rest.Client> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,12 +56,12 @@ export const Provider = Context.Provider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useSessions() {
 | 
					export function useSessions() {
 | 
				
			||||||
  const sessions = useSessionsRaw();
 | 
					  const sessions = useSessionsRaw();
 | 
				
			||||||
  const {push} = useNavigator();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  const location = useLocation();
 | 
					  const location = useLocation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createRenderEffect(() => {
 | 
					  createRenderEffect(() => {
 | 
				
			||||||
    if (sessions().length > 0) return;
 | 
					    if (sessions().length > 0) return;
 | 
				
			||||||
    push(
 | 
					    navigate(
 | 
				
			||||||
      "/accounts/sign-in?back=" + encodeURIComponent(location.pathname),
 | 
					      "/accounts/sign-in?back=" + encodeURIComponent(location.pathname),
 | 
				
			||||||
      { replace: true },
 | 
					      { replace: true },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +25,22 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  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,32 +1,18 @@
 | 
				
			||||||
.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;
 | 
				
			||||||
| 
						 | 
					@ -34,9 +20,3 @@
 | 
				
			||||||
  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,48 +28,42 @@ 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="topbar" ref={setTopbarElement} role="presentation">
 | 
					        <div class="Scaffold__topbar" ref={setTopbarElement} role="presentation">
 | 
				
			||||||
          {props.topbar}
 | 
					          {props.topbar}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </Show>
 | 
					      </Show>
 | 
				
			||||||
      <Show when={props.fab}>
 | 
					      <Show when={props.fab}>
 | 
				
			||||||
        <div class="fab-dock" role="presentation">
 | 
					        <div class="Scaffold__fab-dock" role="presentation">{props.fab}</div>
 | 
				
			||||||
          {props.fab}
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </Show>
 | 
					      </Show>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        ref={(e) => {
 | 
				
			||||||
 | 
					          createRenderEffect(() => {
 | 
				
			||||||
 | 
					            e.style.setProperty(
 | 
				
			||||||
 | 
					              "--scaffold-topbar-height",
 | 
				
			||||||
 | 
					              (topbarSize.height?.toString() ?? 0) + "px",
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {managed.children}
 | 
					          if (managed.ref) {
 | 
				
			||||||
 | 
					            (managed.ref as (val: typeof e) => void)(e);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        {...rest}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {managed.children}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
      <Show when={props.bottom}>
 | 
					      <Show when={props.bottom}>
 | 
				
			||||||
        <div class="bottom-dock" role="presentation">
 | 
					        <div class="Scaffold__bottom-dock" role="presentation">{props.bottom}</div>
 | 
				
			||||||
          {props.bottom}
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </Show>
 | 
					      </Show>
 | 
				
			||||||
    </div>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,21 +0,0 @@
 | 
				
			||||||
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;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,24 +0,0 @@
 | 
				
			||||||
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;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,59 +0,0 @@
 | 
				
			||||||
.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;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,432 +0,0 @@
 | 
				
			||||||
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,6 +1,4 @@
 | 
				
			||||||
.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 { useParams } from "@solidjs/router";
 | 
					import { useNavigate, 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,10 +57,9 @@ 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 { pop } = useNavigator();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  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);
 | 
				
			||||||
| 
						 | 
					@ -210,7 +209,11 @@ const Profile: Component = () => {
 | 
				
			||||||
              paddingTop: "var(--safe-area-inset-top)",
 | 
					              paddingTop: "var(--safe-area-inset-top)",
 | 
				
			||||||
            }}
 | 
					            }}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <IconButton color="inherit" onClick={[pop, 1]} aria-label="Close">
 | 
					            <IconButton
 | 
				
			||||||
 | 
					              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 { useNavigator } from "../platform/StackedRouter";
 | 
					import { useNavigate } from "@solidjs/router";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ChooseLang: Component = () => {
 | 
					const ChooseLang: Component = () => {
 | 
				
			||||||
  const { pop } = useNavigator();
 | 
					  const navigate = useNavigate()
 | 
				
			||||||
  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={[pop, 1]} disableRipple>
 | 
					            <IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
 | 
				
			||||||
              <ArrowBack />
 | 
					              <ArrowBack />
 | 
				
			||||||
            </IconButton>
 | 
					            </IconButton>
 | 
				
			||||||
            <Title>{t("Choose Language")}</Title>
 | 
					            <Title>{t("Choose Language")}</Title>
 | 
				
			||||||
| 
						 | 
					@ -96,10 +96,7 @@ const ChooseLang: Component = () => {
 | 
				
			||||||
                <ListItemText>{t(`lang.${c}`)}</ListItemText>
 | 
					                <ListItemText>{t(`lang.${c}`)}</ListItemText>
 | 
				
			||||||
                <ListItemSecondaryAction>
 | 
					                <ListItemSecondaryAction>
 | 
				
			||||||
                  <Radio
 | 
					                  <Radio
 | 
				
			||||||
                    checked={
 | 
					                    checked={code() === c || (code() === undefined && matchedLangCode() == c)}
 | 
				
			||||||
                      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 {pop} = useNavigator();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  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={[pop, 1]} disableRipple>
 | 
					            <IconButton color="inherit" onClick={[navigate, -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 {pop} = useNavigator();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  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={[pop, 1]} disableRipple>
 | 
					            <IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
 | 
				
			||||||
              <ArrowBack />
 | 
					              <ArrowBack />
 | 
				
			||||||
            </IconButton>
 | 
					            </IconButton>
 | 
				
			||||||
            <Title>{t("Choose Region")}</Title>
 | 
					            <Title>{t("Choose Region")}</Title>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,10 @@
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  children,
 | 
				
			||||||
 | 
					  createSignal,
 | 
				
			||||||
  For,
 | 
					  For,
 | 
				
			||||||
  Show,
 | 
					  Show,
 | 
				
			||||||
  type Component,
 | 
					  type JSX,
 | 
				
			||||||
 | 
					  type ParentComponent,
 | 
				
			||||||
} from "solid-js";
 | 
					} from "solid-js";
 | 
				
			||||||
import Scaffold from "../material/Scaffold.js";
 | 
					import Scaffold from "../material/Scaffold.js";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -27,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 from "../platform/A.js";
 | 
					import { A, useNavigate } from "@solidjs/router";
 | 
				
			||||||
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";
 | 
				
			||||||
| 
						 | 
					@ -41,9 +44,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;
 | 
				
			||||||
| 
						 | 
					@ -159,7 +162,7 @@ type Strings = {
 | 
				
			||||||
  ["lang.auto"]: Template<{ detected: string }>;
 | 
					  ["lang.auto"]: Template<{ detected: string }>;
 | 
				
			||||||
} & Record<string, string | undefined>;
 | 
					} & Record<string, string | undefined>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Settings: Component = () => {
 | 
					const Settings: ParentComponent = (props) => {
 | 
				
			||||||
  const [t] = createTranslator(
 | 
					  const [t] = createTranslator(
 | 
				
			||||||
    (code) =>
 | 
					    (code) =>
 | 
				
			||||||
      import(`./i18n/${code}.json`) as Promise<{
 | 
					      import(`./i18n/${code}.json`) as Promise<{
 | 
				
			||||||
| 
						 | 
					@ -167,9 +170,9 @@ const Settings: Component = () => {
 | 
				
			||||||
      }>,
 | 
					      }>,
 | 
				
			||||||
    () => import(`./i18n/lang-names.json`),
 | 
					    () => import(`./i18n/lang-names.json`),
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const {pop} = useNavigator();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  const settings$ = useStore($settings);
 | 
					  const settings$ = useStore($settings);
 | 
				
			||||||
  const { needRefresh } = useServiceWorker();
 | 
					  const { needRefresh, offlineReady } = useServiceWorker();
 | 
				
			||||||
  const dateFnLocale = useDateFnLocale();
 | 
					  const dateFnLocale = useDateFnLocale();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const profiles = useSessions();
 | 
					  const profiles = useSessions();
 | 
				
			||||||
| 
						 | 
					@ -178,6 +181,8 @@ const Settings: Component = () => {
 | 
				
			||||||
    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;
 | 
				
			||||||
| 
						 | 
					@ -195,7 +200,7 @@ const Settings: 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={[pop, 1]} disableRipple>
 | 
					            <IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
 | 
				
			||||||
              <CloseIcon />
 | 
					              <CloseIcon />
 | 
				
			||||||
            </IconButton>
 | 
					            </IconButton>
 | 
				
			||||||
            <Title>{t("Settings")}</Title>
 | 
					            <Title>{t("Settings")}</Title>
 | 
				
			||||||
| 
						 | 
					@ -203,6 +208,10 @@ const Settings: Component = () => {
 | 
				
			||||||
        </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,9 +3,11 @@ import {
 | 
				
			||||||
  Show,
 | 
					  Show,
 | 
				
			||||||
  onMount,
 | 
					  onMount,
 | 
				
			||||||
  type ParentComponent,
 | 
					  type ParentComponent,
 | 
				
			||||||
  createRenderEffect,
 | 
					  children,
 | 
				
			||||||
 | 
					  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,
 | 
				
			||||||
| 
						 | 
					@ -21,8 +23,14 @@ 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";
 | 
				
			||||||
| 
						 | 
					@ -35,17 +43,29 @@ 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 = () => {
 | 
				
			||||||
| 
						 | 
					@ -82,17 +102,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);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -115,6 +135,30 @@ 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 {
 | 
				
			||||||
| 
						 | 
					@ -165,7 +209,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>
 | 
					              <Tabs onFocusChanged={setCurrentFocusOn} offset={panelOffset()}>
 | 
				
			||||||
                <Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
 | 
					                <Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
 | 
				
			||||||
                  Home
 | 
					                  Home
 | 
				
			||||||
                </Tab>
 | 
					                </Tab>
 | 
				
			||||||
| 
						 | 
					@ -195,40 +239,48 @@ const Home: ParentComponent = (props) => {
 | 
				
			||||||
          </AppBar>
 | 
					          </AppBar>
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <TimeSourceProvider value={now}>
 | 
					        <HeroSourceProvider value={[heroSrc, setHeroSrc]}>
 | 
				
			||||||
          <Show when={!!client()}>
 | 
					          <TimeSourceProvider value={now}>
 | 
				
			||||||
            <div
 | 
					            <Show when={!!client()}>
 | 
				
			||||||
              class="panel-list"
 | 
					              <div class="panel-list" ref={panelList!}>
 | 
				
			||||||
              ref={panelList!}
 | 
					                <div class="tab-panel">
 | 
				
			||||||
              onScroll={requestRecalculateTabIndicator}
 | 
					                  <div>
 | 
				
			||||||
            >
 | 
					                    <TimelinePanel
 | 
				
			||||||
              <div class="tab-panel">
 | 
					                      client={client()}
 | 
				
			||||||
                <div>
 | 
					                      name="home"
 | 
				
			||||||
                  <TimelinePanel
 | 
					                      prefetch={prefetching()}
 | 
				
			||||||
                    client={client()}
 | 
					                      openFullScreenToot={openFullScreenToot}
 | 
				
			||||||
                    name="home"
 | 
					                    />
 | 
				
			||||||
                    prefetch={prefetching()}
 | 
					                  </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">
 | 
					                  <div>
 | 
				
			||||||
                <div>
 | 
					                    <TimelinePanel
 | 
				
			||||||
                  <TimelinePanel
 | 
					                      client={client()}
 | 
				
			||||||
                    client={client()}
 | 
					                      name="public"
 | 
				
			||||||
                    name="public"
 | 
					                      prefetch={prefetching()}
 | 
				
			||||||
                    prefetch={prefetching()}
 | 
					                      openFullScreenToot={openFullScreenToot}
 | 
				
			||||||
                  />
 | 
					                    />
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div></div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div></div>
 | 
					            </Show>
 | 
				
			||||||
            </div>
 | 
					          </TimeSourceProvider>
 | 
				
			||||||
          </Show>
 | 
					          <Suspense>
 | 
				
			||||||
        </TimeSourceProvider>
 | 
					            <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 "../platform/A";
 | 
					import { A } from "@solidjs/router";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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">
 | 
					        <MenuItem component={A} href="/settings" onClick={onClose}>
 | 
				
			||||||
          <ListItemIcon>
 | 
					          <ListItemIcon>
 | 
				
			||||||
            <SettingsIcon />
 | 
					            <SettingsIcon />
 | 
				
			||||||
          </ListItemIcon>
 | 
					          </ListItemIcon>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,6 @@ 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;
 | 
				
			||||||
| 
						 | 
					@ -34,7 +33,6 @@ const PullDownToRefresh: Component<{
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const rootVisible = obvx(() => rootElement);
 | 
					  const rootVisible = obvx(() => rootElement);
 | 
				
			||||||
  const isFrameSuspended = useMaybeIsFrameSuspended()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createEffect(() => {
 | 
					  createEffect(() => {
 | 
				
			||||||
    if (!rootVisible()) setPullDown(0);
 | 
					    if (!rootVisible()) setPullDown(0);
 | 
				
			||||||
| 
						 | 
					@ -111,9 +109,6 @@ 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);
 | 
				
			||||||
| 
						 | 
					@ -164,9 +159,6 @@ 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,6 +20,12 @@ 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,11 +1,12 @@
 | 
				
			||||||
.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: calc(100% - var(--scaffold-topbar-height, 0px));
 | 
					    height: 100%;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .progress-line {
 | 
					  .progress-line {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,10 @@
 | 
				
			||||||
import { useLocation, useParams } from "@solidjs/router";
 | 
					import { useLocation, useNavigate, 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";
 | 
				
			||||||
| 
						 | 
					@ -24,8 +25,6 @@ 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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,7 +43,7 @@ const TootBottomSheet: Component = (props) => {
 | 
				
			||||||
  const location = useLocation<{
 | 
					  const location = useLocation<{
 | 
				
			||||||
    tootReply?: boolean;
 | 
					    tootReply?: boolean;
 | 
				
			||||||
  }>();
 | 
					  }>();
 | 
				
			||||||
  const { pop, push } = useNavigator();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  const time = createTimeSource();
 | 
					  const time = createTimeSource();
 | 
				
			||||||
  const acctText = () => decodeURIComponent(params.acct);
 | 
					  const acctText = () => decodeURIComponent(params.acct);
 | 
				
			||||||
  const session = useSessionForAcctStr(acctText);
 | 
					  const session = useSessionForAcctStr(acctText);
 | 
				
			||||||
| 
						 | 
					@ -187,7 +186,7 @@ const TootBottomSheet: Component = (props) => {
 | 
				
			||||||
          target.dataset.client || `@${new URL(target.href).origin}`,
 | 
					          target.dataset.client || `@${new URL(target.href).origin}`,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        push(`/${acct}/profile/${target.dataset.acctId}`);
 | 
					        navigate(`/${acct}/profile/${target.dataset.acctId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
| 
						 | 
					@ -229,7 +228,9 @@ const TootBottomSheet: Component = (props) => {
 | 
				
			||||||
            variant="dense"
 | 
					            variant="dense"
 | 
				
			||||||
            sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
 | 
					            sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <BackButton color="inherit" />
 | 
					            <IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
 | 
				
			||||||
 | 
					              <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) =>
 | 
				
			||||||
| 
						 | 
					@ -245,7 +246,9 @@ const TootBottomSheet: Component = (props) => {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      class="TootBottomSheet"
 | 
					      class="TootBottomSheet"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div class="Scrollable">
 | 
					      <div
 | 
				
			||||||
 | 
					        class="Scrollable"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        <TimeSourceProvider value={time}>
 | 
					        <TimeSourceProvider value={time}>
 | 
				
			||||||
          <TootList
 | 
					          <TootList
 | 
				
			||||||
            threads={ancestors.list}
 | 
					            threads={ancestors.list}
 | 
				
			||||||
| 
						 | 
					@ -285,7 +288,9 @@ const TootBottomSheet: Component = (props) => {
 | 
				
			||||||
          </Show>
 | 
					          </Show>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Show when={tootContextErrorUncaught.loading}>
 | 
					          <Show when={tootContextErrorUncaught.loading}>
 | 
				
			||||||
            <div class="progress-line">
 | 
					            <div
 | 
				
			||||||
 | 
					              class="progress-line"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
              <CircularProgress style="width: 1.5em; height: 1.5em;" />
 | 
					              <CircularProgress style="width: 1.5em; height: 1.5em;" />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </Show>
 | 
					          </Show>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,28 +13,13 @@ 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) {
 | 
				
			||||||
| 
						 | 
					@ -55,7 +40,7 @@ const TootList: Component<{
 | 
				
			||||||
  const session = useDefaultSession();
 | 
					  const session = useDefaultSession();
 | 
				
			||||||
  const heroSrc = useHeroSource();
 | 
					  const heroSrc = useHeroSource();
 | 
				
			||||||
  const [expandedThreadId, setExpandedThreadId] = createSignal<string>();
 | 
					  const [expandedThreadId, setExpandedThreadId] = createSignal<string>();
 | 
				
			||||||
  const { push } = useNavigator();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onBookmark = async (status: mastodon.v1.Status) => {
 | 
					  const onBookmark = async (status: mastodon.v1.Status) => {
 | 
				
			||||||
    const client = session()?.client;
 | 
					    const client = session()?.client;
 | 
				
			||||||
| 
						 | 
					@ -114,7 +99,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;
 | 
				
			||||||
| 
						 | 
					@ -130,55 +115,12 @@ 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
 | 
				
			||||||
      animateOpen(element) {
 | 
					        ? {
 | 
				
			||||||
        const rect0 = srcElement.getBoundingClientRect(); // the start rect
 | 
					            tootReply: true,
 | 
				
			||||||
        const rect1 = element.getBoundingClientRect(); // the end rect
 | 
					          }
 | 
				
			||||||
 | 
					        : undefined,
 | 
				
			||||||
        const duration = durationOf(rect0, rect1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const keyframes = {
 | 
					 | 
				
			||||||
          top: [`${rect0.top}px`, `${rect1.top}px`],
 | 
					 | 
				
			||||||
          bottom: [`${rect0.bottom}px`, `${rect1.bottom}px`],
 | 
					 | 
				
			||||||
          left: [`${rect0.left}px`, `${rect1.left}px`],
 | 
					 | 
				
			||||||
          right: [`${rect0.right}px`, `${rect1.right}px`],
 | 
					 | 
				
			||||||
          height: [`${rect0.height}px`, `${rect1.height}px`],
 | 
					 | 
				
			||||||
          margin: 0,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        srcElement.style.visibility = "hidden";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const animation = element.animate(keyframes, {
 | 
					 | 
				
			||||||
          duration,
 | 
					 | 
				
			||||||
          easing: ANIM_CURVE_STD,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        return animation;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      animateClose(element) {
 | 
					 | 
				
			||||||
        const rect0 = element.getBoundingClientRect(); // the start rect
 | 
					 | 
				
			||||||
        const rect1 = srcElement.getBoundingClientRect(); // the end rect
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const duration = durationOf(rect0, rect1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const keyframes = {
 | 
					 | 
				
			||||||
          top: [`${rect0.top}px`, `${rect1.top}px`],
 | 
					 | 
				
			||||||
          bottom: [`${rect0.bottom}px`, `${rect1.bottom}px`],
 | 
					 | 
				
			||||||
          left: [`${rect0.left}px`, `${rect1.left}px`],
 | 
					 | 
				
			||||||
          right: [`${rect0.right}px`, `${rect1.right}px`],
 | 
					 | 
				
			||||||
          height: [`${rect0.height}px`, `${rect1.height}px`],
 | 
					 | 
				
			||||||
          margin: 0,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        srcElement.style.visibility = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const animation = element.animate(keyframes, {
 | 
					 | 
				
			||||||
          duration,
 | 
					 | 
				
			||||||
          easing: ANIM_CURVE_STD,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        return animation;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -204,7 +146,7 @@ const TootList: Component<{
 | 
				
			||||||
          target.dataset.client || `@${new URL(target.href).origin}`,
 | 
					          target.dataset.client || `@${new URL(target.href).origin}`,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        push(`/${acct}/profile/${target.dataset.acctId}`);
 | 
					        navigate(`/${acct}/profile/${target.dataset.acctId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,12 @@ 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…
	
	Add table
		Add a link
		
	
		Reference in a new issue