* add ~platform/DocumentTitle * add titles for some pages
This commit is contained in:
parent
1115135380
commit
1c8a3f0bbb
12 changed files with 843 additions and 823 deletions
|
@ -8,13 +8,13 @@ import {
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { acceptAccountViaAuthCode } from "./stores";
|
import { acceptAccountViaAuthCode } from "./stores";
|
||||||
import { $settings } from "../settings/stores";
|
import { $settings } from "../settings/stores";
|
||||||
import { useDocumentTitle } from "../utils";
|
|
||||||
import cards from "~material/cards.module.css";
|
import cards from "~material/cards.module.css";
|
||||||
import { LinearProgress } from "@suid/material";
|
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";
|
import { useNavigator } from "~platform/StackedRouter";
|
||||||
|
import DocumentTitle from "~platform/DocumentTitle";
|
||||||
|
|
||||||
type OAuth2CallbackParams = {
|
type OAuth2CallbackParams = {
|
||||||
code?: string;
|
code?: string;
|
||||||
|
@ -27,13 +27,12 @@ const MastodonOAuth2Callback: Component = () => {
|
||||||
const titleId = createUniqueId();
|
const titleId = createUniqueId();
|
||||||
const [params] = useSearchParams<OAuth2CallbackParams>();
|
const [params] = useSearchParams<OAuth2CallbackParams>();
|
||||||
const { push: navigate } = useNavigator();
|
const { push: navigate } = useNavigator();
|
||||||
const setDocumentTitle = useDocumentTitle("Back from Mastodon...");
|
|
||||||
const [siteImg, setSiteImg] = createSignal<{
|
const [siteImg, setSiteImg] = createSignal<{
|
||||||
src: string;
|
src: string;
|
||||||
srcset?: string;
|
srcset?: string;
|
||||||
blurhash: string;
|
blurhash: string;
|
||||||
}>();
|
}>();
|
||||||
const [siteTitle, setSiteTitle] = createSignal("the Mastodon server");
|
const [siteTitle, setSiteTitle] = createSignal("Mastodon");
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const onGoingOAuth2Process = $settings.get().onGoingOAuth2Process;
|
const onGoingOAuth2Process = $settings.get().onGoingOAuth2Process;
|
||||||
|
@ -42,7 +41,6 @@ const MastodonOAuth2Callback: Component = () => {
|
||||||
url: onGoingOAuth2Process,
|
url: onGoingOAuth2Process,
|
||||||
});
|
});
|
||||||
const ins = await client.v2.instance.fetch();
|
const ins = await client.v2.instance.fetch();
|
||||||
setDocumentTitle(`Back from ${ins.title}...`);
|
|
||||||
setSiteTitle(ins.title);
|
setSiteTitle(ins.title);
|
||||||
|
|
||||||
const srcset = [];
|
const srcset = [];
|
||||||
|
@ -93,42 +91,45 @@ const MastodonOAuth2Callback: Component = () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div class={cards.layoutCentered}>
|
<>
|
||||||
<div class={cards.card} aria-busy="true" aria-describedby={progressId}>
|
<DocumentTitle>Back from {siteTitle()}</DocumentTitle>
|
||||||
<LinearProgress
|
<div class={cards.layoutCentered}>
|
||||||
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
|
<div class={cards.card} aria-busy="true" aria-describedby={progressId}>
|
||||||
id={progressId}
|
<LinearProgress
|
||||||
aria-labelledby={titleId}
|
|
||||||
/>
|
|
||||||
<Show
|
|
||||||
when={siteImg()}
|
|
||||||
fallback={
|
|
||||||
<i
|
|
||||||
aria-busy="true"
|
|
||||||
aria-label="Preparing image..."
|
|
||||||
style={{ height: "235px", display: "block" }}
|
|
||||||
></i>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Img
|
|
||||||
src={siteImg()?.src}
|
|
||||||
srcset={siteImg()?.srcset}
|
|
||||||
blurhash={siteImg()?.blurhash}
|
|
||||||
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
|
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
|
||||||
alt={`Banner image for ${siteTitle()}`}
|
id={progressId}
|
||||||
style={{ height: "235px", display: "block" }}
|
aria-labelledby={titleId}
|
||||||
/>
|
/>
|
||||||
</Show>
|
<Show
|
||||||
|
when={siteImg()}
|
||||||
|
fallback={
|
||||||
|
<i
|
||||||
|
aria-busy="true"
|
||||||
|
aria-label="Preparing image..."
|
||||||
|
style={{ height: "235px", display: "block" }}
|
||||||
|
></i>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Img
|
||||||
|
src={siteImg()?.src}
|
||||||
|
srcset={siteImg()?.srcset}
|
||||||
|
blurhash={siteImg()?.blurhash}
|
||||||
|
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
|
||||||
|
alt={`Banner image for ${siteTitle()}`}
|
||||||
|
style={{ height: "235px", display: "block" }}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Title component="h6" id={titleId}>
|
<Title component="h6" id={titleId}>
|
||||||
Contracting {siteTitle}...
|
Contracting {siteTitle}...
|
||||||
</Title>
|
</Title>
|
||||||
<p>
|
<p>
|
||||||
If this page stays too long, you can close this page and sign in
|
If this page stays too long, you can close this page and sign in
|
||||||
again.
|
again.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ import {
|
||||||
Component,
|
Component,
|
||||||
Show,
|
Show,
|
||||||
createEffect,
|
createEffect,
|
||||||
createSelector,
|
|
||||||
createSignal,
|
createSignal,
|
||||||
createUniqueId,
|
createUniqueId,
|
||||||
onMount,
|
onMount,
|
||||||
|
@ -10,15 +9,14 @@ import {
|
||||||
import cards from "~material/cards.module.css";
|
import cards from "~material/cards.module.css";
|
||||||
import TextField from "~material/TextField.js";
|
import TextField from "~material/TextField.js";
|
||||||
import Button from "~material/Button.js";
|
import Button from "~material/Button.js";
|
||||||
import { useDocumentTitle } from "../utils";
|
|
||||||
import { Title } from "~material/typography";
|
import { Title } from "~material/typography";
|
||||||
import { css } from "solid-styled";
|
|
||||||
import { LinearProgress } from "@suid/material";
|
import { LinearProgress } from "@suid/material";
|
||||||
import { createRestAPIClient } from "masto";
|
import { createRestAPIClient } from "masto";
|
||||||
import { getOrRegisterApp } from "./stores";
|
import { getOrRegisterApp } from "./stores";
|
||||||
import { useSearchParams } from "@solidjs/router";
|
import { useSearchParams } from "@solidjs/router";
|
||||||
import { $settings } from "../settings/stores";
|
import { $settings } from "../settings/stores";
|
||||||
import "./SignIn.css";
|
import "./SignIn.css";
|
||||||
|
import DocumentTitle from "~platform/DocumentTitle";
|
||||||
|
|
||||||
type ErrorParams = {
|
type ErrorParams = {
|
||||||
error: string;
|
error: string;
|
||||||
|
@ -36,8 +34,6 @@ const SignIn: Component = () => {
|
||||||
const [serverUrlError, setServerUrlError] = createSignal(false);
|
const [serverUrlError, setServerUrlError] = createSignal(false);
|
||||||
const [targetSiteTitle, setTargetSiteTitle] = createSignal("");
|
const [targetSiteTitle, setTargetSiteTitle] = createSignal("");
|
||||||
|
|
||||||
useDocumentTitle("Sign In");
|
|
||||||
|
|
||||||
const serverUrl = () => {
|
const serverUrl = () => {
|
||||||
const url = rawServerUrl();
|
const url = rawServerUrl();
|
||||||
if (url.length === 0 || /^%w:/.test(url)) {
|
if (url.length === 0 || /^%w:/.test(url)) {
|
||||||
|
@ -115,55 +111,58 @@ const SignIn: Component = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main class="SignIn">
|
<>
|
||||||
<Show when={params.error || params.errorDescription}>
|
<DocumentTitle>Sign In</DocumentTitle>
|
||||||
<div class={cards.card} style={{ "margin-bottom": "20px" }}>
|
<main class="SignIn">
|
||||||
<p>Authorization is failed.</p>
|
<Show when={params.error || params.errorDescription}>
|
||||||
<p>{params.errorDescription}</p>
|
<div class={cards.card} style={{ "margin-bottom": "20px" }}>
|
||||||
<p>
|
<p>Authorization is failed.</p>
|
||||||
Please try again later. If the problem persists, you can ask for
|
<p>{params.errorDescription}</p>
|
||||||
help from the server administrator.
|
<p>
|
||||||
</p>
|
Please try again later. If the problem persists, you can ask for
|
||||||
</div>
|
help from the server administrator.
|
||||||
</Show>
|
</p>
|
||||||
<div
|
|
||||||
class={`${cards.card} key-content`}
|
|
||||||
aria-busy={currentState() !== "inactive" ? "true" : "false"}
|
|
||||||
aria-describedby={
|
|
||||||
currentState() !== "inactive" ? progressId : undefined
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
padding: `var(--safe-area-inset-top) var(--safe-area-inset-right) var(--safe-area-inset-bottom) var(--safe-area-inset-left)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LinearProgress
|
|
||||||
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
|
|
||||||
id={progressId}
|
|
||||||
sx={currentState() === "inactive" ? { display: "none" } : undefined}
|
|
||||||
/>
|
|
||||||
<form onSubmit={onStartOAuth2}>
|
|
||||||
<Title component="h6">Sign in with Your Mastodon Account</Title>
|
|
||||||
<TextField
|
|
||||||
label="Mastodon Server"
|
|
||||||
name="serverUrl"
|
|
||||||
onInput={setRawServerUrl}
|
|
||||||
required
|
|
||||||
helperText={serverUrlHelperText()}
|
|
||||||
error={!!serverUrlError()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div style={{ display: "flex", "justify-content": "end" }}>
|
|
||||||
<Button type="submit" disabled={currentState() !== "inactive"}>
|
|
||||||
{currentState() == "inactive"
|
|
||||||
? "Continue"
|
|
||||||
: currentState() == "contracting"
|
|
||||||
? `Contracting ${new URL(serverUrl()).host}...`
|
|
||||||
: `Moving to ${targetSiteTitle}`}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</Show>
|
||||||
</div>
|
<div
|
||||||
</main>
|
class={`${cards.card} key-content`}
|
||||||
|
aria-busy={currentState() !== "inactive" ? "true" : "false"}
|
||||||
|
aria-describedby={
|
||||||
|
currentState() !== "inactive" ? progressId : undefined
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
padding: `var(--safe-area-inset-top) var(--safe-area-inset-right) var(--safe-area-inset-bottom) var(--safe-area-inset-left)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LinearProgress
|
||||||
|
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
|
||||||
|
id={progressId}
|
||||||
|
sx={currentState() === "inactive" ? { display: "none" } : undefined}
|
||||||
|
/>
|
||||||
|
<form onSubmit={onStartOAuth2}>
|
||||||
|
<Title component="h6">Sign in with Your Mastodon Account</Title>
|
||||||
|
<TextField
|
||||||
|
label="Mastodon Server"
|
||||||
|
name="serverUrl"
|
||||||
|
onInput={setRawServerUrl}
|
||||||
|
required
|
||||||
|
helperText={serverUrlHelperText()}
|
||||||
|
error={!!serverUrlError()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", "justify-content": "end" }}>
|
||||||
|
<Button type="submit" disabled={currentState() !== "inactive"}>
|
||||||
|
{currentState() == "inactive"
|
||||||
|
? "Continue"
|
||||||
|
: currentState() == "contracting"
|
||||||
|
? `Contracting ${new URL(serverUrl()).host}...`
|
||||||
|
: `Moving to ${targetSiteTitle}`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,12 @@ import {
|
||||||
splitProps,
|
splitProps,
|
||||||
Component,
|
Component,
|
||||||
createSignal,
|
createSignal,
|
||||||
createEffect,
|
|
||||||
onMount,
|
onMount,
|
||||||
createRenderEffect,
|
createRenderEffect,
|
||||||
Show,
|
Show,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { css } from "solid-styled";
|
import { css } from "solid-styled";
|
||||||
import { decode } from "blurhash";
|
import { decode } from "blurhash";
|
||||||
import { mergeClass } from "../utils";
|
|
||||||
|
|
||||||
type ImgProps = {
|
type ImgProps = {
|
||||||
blurhash?: string;
|
blurhash?: string;
|
||||||
|
@ -24,6 +22,7 @@ const Img: Component<ImgProps> = (props) => {
|
||||||
"blurhash",
|
"blurhash",
|
||||||
"keepBlur",
|
"keepBlur",
|
||||||
"class",
|
"class",
|
||||||
|
"classList",
|
||||||
"style",
|
"style",
|
||||||
]);
|
]);
|
||||||
const [isImgLoaded, setIsImgLoaded] = createSignal(false);
|
const [isImgLoaded, setIsImgLoaded] = createSignal(false);
|
||||||
|
@ -61,21 +60,21 @@ const Img: Component<ImgProps> = (props) => {
|
||||||
const onImgLoaded = () => {
|
const onImgLoaded = () => {
|
||||||
setIsImgLoaded(true);
|
setIsImgLoaded(true);
|
||||||
setImgSize({
|
setImgSize({
|
||||||
width: imgE.width,
|
width: imgE!.width,
|
||||||
height: imgE.height,
|
height: imgE!.height,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMetadataLoaded = () => {
|
const onMetadataLoaded = () => {
|
||||||
setImgSize({
|
setImgSize({
|
||||||
width: imgE.width,
|
width: imgE!.width,
|
||||||
height: imgE.height,
|
height: imgE!.height,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setImgSize((x) => {
|
setImgSize((x) => {
|
||||||
const parent = imgE.parentElement;
|
const parent = imgE!.parentElement;
|
||||||
if (!parent) return x;
|
if (!parent) return x;
|
||||||
return x
|
return x
|
||||||
? x
|
? x
|
||||||
|
@ -87,7 +86,14 @@ const Img: Component<ImgProps> = (props) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={mergeClass(managed.class, "img-root")} style={managed.style}>
|
<div
|
||||||
|
classList={{
|
||||||
|
...managed.classList,
|
||||||
|
[managed.class ?? ""]: true,
|
||||||
|
"img-root": true,
|
||||||
|
}}
|
||||||
|
style={managed.style}
|
||||||
|
>
|
||||||
<Show when={managed.blurhash}>
|
<Show when={managed.blurhash}>
|
||||||
<canvas
|
<canvas
|
||||||
ref={(canvas) => {
|
ref={(canvas) => {
|
||||||
|
|
22
src/platform/DocumentTitle.tsx
Normal file
22
src/platform/DocumentTitle.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { children, createRenderEffect, onCleanup, type JSX } from "solid-js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Document title.
|
||||||
|
*
|
||||||
|
* The `children` must be plain text.
|
||||||
|
*/
|
||||||
|
export default function (props: { children?: JSX.Element }) {
|
||||||
|
let otitle: string | undefined;
|
||||||
|
|
||||||
|
createRenderEffect(() => (otitle = document.title));
|
||||||
|
|
||||||
|
const title = children(() => props.children);
|
||||||
|
|
||||||
|
createRenderEffect(
|
||||||
|
() => (document.title = (title.toArray() as string[]).join("")),
|
||||||
|
);
|
||||||
|
|
||||||
|
onCleanup(() => (document.title = otitle!));
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
}
|
|
@ -65,6 +65,7 @@ import {
|
||||||
} from "../timelines/toots/ItemSelectionProvider";
|
} from "../timelines/toots/ItemSelectionProvider";
|
||||||
import AppTopBar from "~material/AppTopBar";
|
import AppTopBar from "~material/AppTopBar";
|
||||||
import type { Account } from "../accounts/stores";
|
import type { Account } from "../accounts/stores";
|
||||||
|
import DocumentTitle from "~platform/DocumentTitle";
|
||||||
|
|
||||||
const Profile: Component = () => {
|
const Profile: Component = () => {
|
||||||
const { pop } = useNavigator();
|
const { pop } = useNavigator();
|
||||||
|
@ -216,355 +217,360 @@ const Profile: Component = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Scaffold
|
<>
|
||||||
topbar={
|
<DocumentTitle>{profile()?.displayName ?? "Someone"}</DocumentTitle>
|
||||||
<AppTopBar
|
<Scaffold
|
||||||
role="navigation"
|
topbar={
|
||||||
position="static"
|
<AppTopBar
|
||||||
color={scrolledPastBanner() ? "primary" : "transparent"}
|
role="navigation"
|
||||||
elevation={scrolledPastBanner() ? undefined : 0}
|
position="static"
|
||||||
style={{
|
color={scrolledPastBanner() ? "primary" : "transparent"}
|
||||||
color: scrolledPastBanner()
|
elevation={scrolledPastBanner() ? undefined : 0}
|
||||||
? undefined
|
|
||||||
: bannerSampledColors()?.text,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton color="inherit" onClick={[pop, 1]} aria-label="Close">
|
|
||||||
<Close />
|
|
||||||
</IconButton>
|
|
||||||
<Title
|
|
||||||
class="Profile__page-title"
|
|
||||||
style={{
|
style={{
|
||||||
visibility: scrolledPastBanner() ? undefined : "hidden",
|
color: scrolledPastBanner()
|
||||||
|
? undefined
|
||||||
|
: bannerSampledColors()?.text,
|
||||||
}}
|
}}
|
||||||
innerHTML={displayName()}
|
|
||||||
></Title>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
id={menuButId}
|
|
||||||
aria-controls={optMenuId}
|
|
||||||
color="inherit"
|
|
||||||
onClick={[setMenuOpen, true]}
|
|
||||||
aria-label="Open Options for the Profile"
|
|
||||||
>
|
>
|
||||||
<MoreVert />
|
<IconButton color="inherit" onClick={[pop, 1]} aria-label="Close">
|
||||||
</IconButton>
|
<Close />
|
||||||
</AppTopBar>
|
</IconButton>
|
||||||
}
|
<Title
|
||||||
class="Profile"
|
class="Profile__page-title"
|
||||||
>
|
style={{
|
||||||
<div class="details" role="presentation">
|
visibility: scrolledPastBanner() ? undefined : "hidden",
|
||||||
<Menu
|
}}
|
||||||
id={optMenuId}
|
innerHTML={displayName()}
|
||||||
open={menuOpen()}
|
></Title>
|
||||||
onClose={[setMenuOpen, false]}
|
|
||||||
anchor={() =>
|
<IconButton
|
||||||
document.getElementById(menuButId)!.getBoundingClientRect()
|
id={menuButId}
|
||||||
}
|
aria-controls={optMenuId}
|
||||||
aria-label="Options for the Profile"
|
color="inherit"
|
||||||
>
|
onClick={[setMenuOpen, true]}
|
||||||
<Show when={session().account}>
|
aria-label="Open Options for the Profile"
|
||||||
<MenuItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar src={(session().account as Account).inf?.avatar} />
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText secondary={"Default account"}>
|
|
||||||
<span innerHTML={sessionDisplayName()}></span>
|
|
||||||
</ListItemText>
|
|
||||||
{/* <ArrowRight /> // for future */}
|
|
||||||
</MenuItem>
|
|
||||||
</Show>
|
|
||||||
<Show when={session().account && profile()}>
|
|
||||||
<Show
|
|
||||||
when={isCurrentSessionProfile()}
|
|
||||||
fallback={
|
|
||||||
<MenuItem
|
|
||||||
onClick={(event) => {
|
|
||||||
const { left, right, top } =
|
|
||||||
event.currentTarget.getBoundingClientRect();
|
|
||||||
openSubscribeMenu({
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
top,
|
|
||||||
e: 1,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
|
||||||
<PlaylistAdd />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>Subscribe...</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<MenuItem disabled>
|
<MoreVert />
|
||||||
<ListItemIcon>
|
</IconButton>
|
||||||
<Edit />
|
</AppTopBar>
|
||||||
</ListItemIcon>
|
}
|
||||||
<ListItemText>Edit...</ListItemText>
|
class="Profile"
|
||||||
|
>
|
||||||
|
<div class="details" role="presentation">
|
||||||
|
<Menu
|
||||||
|
id={optMenuId}
|
||||||
|
open={menuOpen()}
|
||||||
|
onClose={[setMenuOpen, false]}
|
||||||
|
anchor={() =>
|
||||||
|
document.getElementById(menuButId)!.getBoundingClientRect()
|
||||||
|
}
|
||||||
|
aria-label="Options for the Profile"
|
||||||
|
>
|
||||||
|
<Show when={session().account}>
|
||||||
|
<MenuItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar src={(session().account as Account).inf?.avatar} />
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText secondary={"Default account"}>
|
||||||
|
<span innerHTML={sessionDisplayName()}></span>
|
||||||
|
</ListItemText>
|
||||||
|
{/* <ArrowRight /> // for future */}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Show>
|
</Show>
|
||||||
<Divider />
|
<Show when={session().account && profile()}>
|
||||||
</Show>
|
<Show
|
||||||
<MenuItem disabled>
|
when={isCurrentSessionProfile()}
|
||||||
<ListItemIcon>
|
fallback={
|
||||||
<Group />
|
<MenuItem
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>Followers</ListItemText>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<span aria-label="The number of the account follower">
|
|
||||||
{profile()?.followersCount ?? ""}
|
|
||||||
</span>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem disabled>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Subject />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>Following</ListItemText>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<span aria-label="The number the account following">
|
|
||||||
{profile()?.followingCount ?? ""}
|
|
||||||
</span>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem disabled>
|
|
||||||
<ListItemIcon>
|
|
||||||
<PersonOff />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>Blocklist</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem disabled>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Send />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>Mention in...</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
<Divider />
|
|
||||||
<MenuItem
|
|
||||||
component={"a"}
|
|
||||||
href={profile()?.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
|
||||||
<OpenInBrowser />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>Open in browser...</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={() => share({ url: profile()?.url })}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Share />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>Share...</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: `${268 * (Math.min(560, windowSize.width) / 560)}px`,
|
|
||||||
}}
|
|
||||||
class="banner"
|
|
||||||
role="presentation"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
ref={(e) => obx.observe(e)}
|
|
||||||
src={bannerImg()}
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
alt={`Banner image for ${profile()?.displayName || "the user"}`}
|
|
||||||
onLoad={(event) => {
|
|
||||||
const ins = new FastAverageColor();
|
|
||||||
const colors = ins.getColor(event.currentTarget);
|
|
||||||
setBannerSampledColors({
|
|
||||||
average: colors.hex,
|
|
||||||
text: colors.isDark ? "white" : "black",
|
|
||||||
});
|
|
||||||
ins.destroy();
|
|
||||||
}}
|
|
||||||
></img>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Menu {...subscribeMenuState}>
|
|
||||||
<MenuItem
|
|
||||||
onClick={toggleSubscribeHome}
|
|
||||||
aria-label={`${relationship()?.following ? "Unfollow" : "Follow"} on your home timeline`}
|
|
||||||
>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar src={(session().account as Account).inf?.avatar}></Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
secondary={
|
|
||||||
relationship()?.following
|
|
||||||
? undefined
|
|
||||||
: profile()?.locked
|
|
||||||
? "A request will be sent"
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span innerHTML={sessionDisplayName()}></span>
|
|
||||||
<span>'s Home</span>
|
|
||||||
</ListItemText>
|
|
||||||
|
|
||||||
<Checkbox checked={relationship()?.following ?? false} />
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="intro"
|
|
||||||
style={{
|
|
||||||
"background-color": bannerSampledColors()?.average,
|
|
||||||
color: bannerSampledColors()?.text,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<section class="acct-grp">
|
|
||||||
<Avatar
|
|
||||||
src={avatarImg()}
|
|
||||||
alt={`${profile()?.displayName || "the user"}'s avatar`}
|
|
||||||
sx={{
|
|
||||||
marginTop: "calc(-16px - 72px / 2)",
|
|
||||||
width: "72px",
|
|
||||||
height: "72px",
|
|
||||||
}}
|
|
||||||
></Avatar>
|
|
||||||
<div class="name-grp">
|
|
||||||
<div class="display-name">
|
|
||||||
<Show when={profile()?.bot}>
|
|
||||||
<SmartToySharp class="acct-mark" aria-label="Bot" />
|
|
||||||
</Show>
|
|
||||||
<Show when={profile()?.locked}>
|
|
||||||
<Lock class="acct-mark" aria-label="Locked" />
|
|
||||||
</Show>
|
|
||||||
<Body2
|
|
||||||
component="span"
|
|
||||||
innerHTML={displayName()}
|
|
||||||
aria-label="Display name"
|
|
||||||
></Body2>
|
|
||||||
</div>
|
|
||||||
<span aria-label="Complete username" class="username">
|
|
||||||
{fullUsername()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div role="presentation">
|
|
||||||
<Switch>
|
|
||||||
<Match
|
|
||||||
when={
|
|
||||||
!session().account ||
|
|
||||||
profileUncaught.loading ||
|
|
||||||
profileUncaught.error
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{<></>}
|
|
||||||
</Match>
|
|
||||||
<Match when={isCurrentSessionProfile()}>
|
|
||||||
<IconButton color="inherit">
|
|
||||||
<Edit />
|
|
||||||
</IconButton>
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
openSubscribeMenu(
|
const { left, right, top } =
|
||||||
event.currentTarget.getBoundingClientRect(),
|
event.currentTarget.getBoundingClientRect();
|
||||||
);
|
openSubscribeMenu({
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
top,
|
||||||
|
e: 1,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{relationship()?.following ? "Subscribed" : "Subscribe"}
|
<ListItemIcon>
|
||||||
</Button>
|
<PlaylistAdd />
|
||||||
</Match>
|
</ListItemIcon>
|
||||||
</Switch>
|
<ListItemText>Subscribe...</ListItemText>
|
||||||
</div>
|
</MenuItem>
|
||||||
</section>
|
}
|
||||||
<section
|
>
|
||||||
class="description"
|
<MenuItem disabled>
|
||||||
aria-label={`${profile()?.displayName || "the user"}'s description`}
|
<ListItemIcon>
|
||||||
innerHTML={description() || ""}
|
<Edit />
|
||||||
></section>
|
</ListItemIcon>
|
||||||
|
<ListItemText>Edit...</ListItemText>
|
||||||
<table
|
</MenuItem>
|
||||||
class="acct-fields"
|
</Show>
|
||||||
aria-label={`${profile()?.displayName || "the user"}'s fields`}
|
|
||||||
>
|
|
||||||
<tbody>
|
|
||||||
<For each={profile()?.fields ?? []}>
|
|
||||||
{(item, index) => {
|
|
||||||
return (
|
|
||||||
<tr data-field-index={index()}>
|
|
||||||
<td>{item.name}</td>
|
|
||||||
<td>
|
|
||||||
<Show when={item.verifiedAt}>
|
|
||||||
<Verified />
|
|
||||||
</Show>
|
|
||||||
</td>
|
|
||||||
<td innerHTML={item.value}></td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="recent-toots" role="presentation">
|
|
||||||
<div class="toot-list-toolbar">
|
|
||||||
<TootFilterButton
|
|
||||||
options={{
|
|
||||||
pinned: "Pinneds",
|
|
||||||
boost: "Boosts",
|
|
||||||
reply: "Replies",
|
|
||||||
original: "Originals",
|
|
||||||
}}
|
|
||||||
applied={recentTootFilter()}
|
|
||||||
onApply={setRecentTootFilter}
|
|
||||||
disabledKeys={["original"]}
|
|
||||||
></TootFilterButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ItemSelectionProvider value={selectionState}>
|
|
||||||
<TimeSourceProvider value={time}>
|
|
||||||
<Show
|
|
||||||
when={recentTootFilter().pinned && pinnedToots.list.length > 0}
|
|
||||||
>
|
|
||||||
<TootList
|
|
||||||
threads={pinnedToots.list}
|
|
||||||
onUnknownThread={pinnedToots.getPath}
|
|
||||||
onChangeToot={pinnedToots.set}
|
|
||||||
/>
|
|
||||||
<Divider />
|
<Divider />
|
||||||
</Show>
|
</Show>
|
||||||
<TootList
|
<MenuItem disabled>
|
||||||
id={recentTootListId}
|
<ListItemIcon>
|
||||||
threads={recentToots.list}
|
<Group />
|
||||||
onUnknownThread={recentToots.getPath}
|
</ListItemIcon>
|
||||||
onChangeToot={recentToots.set}
|
<ListItemText>Followers</ListItemText>
|
||||||
/>
|
<ListItemSecondaryAction>
|
||||||
</TimeSourceProvider>
|
<span aria-label="The number of the account follower">
|
||||||
</ItemSelectionProvider>
|
{profile()?.followersCount ?? ""}
|
||||||
|
</span>
|
||||||
<Show when={!recentTootChunk()?.done}>
|
</ListItemSecondaryAction>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem disabled>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Subject />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Following</ListItemText>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<span aria-label="The number the account following">
|
||||||
|
{profile()?.followingCount ?? ""}
|
||||||
|
</span>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem disabled>
|
||||||
|
<ListItemIcon>
|
||||||
|
<PersonOff />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Blocklist</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem disabled>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Send />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Mention in...</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
<Divider />
|
||||||
|
<MenuItem
|
||||||
|
component={"a"}
|
||||||
|
href={profile()?.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<OpenInBrowser />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Open in browser...</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => share({ url: profile()?.url })}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Share />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Share...</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
"text-align": "center",
|
height: `${268 * (Math.min(560, windowSize.width) / 560)}px`,
|
||||||
"padding-bottom": "var(--safe-area-inset-bottom)",
|
}}
|
||||||
|
class="banner"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
ref={(e) => obx.observe(e)}
|
||||||
|
src={bannerImg()}
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
alt={`Banner image for ${profile()?.displayName || "the user"}`}
|
||||||
|
onLoad={(event) => {
|
||||||
|
const ins = new FastAverageColor();
|
||||||
|
const colors = ins.getColor(event.currentTarget);
|
||||||
|
setBannerSampledColors({
|
||||||
|
average: colors.hex,
|
||||||
|
text: colors.isDark ? "white" : "black",
|
||||||
|
});
|
||||||
|
ins.destroy();
|
||||||
|
}}
|
||||||
|
></img>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Menu {...subscribeMenuState}>
|
||||||
|
<MenuItem
|
||||||
|
onClick={toggleSubscribeHome}
|
||||||
|
aria-label={`${relationship()?.following ? "Unfollow" : "Follow"} on your home timeline`}
|
||||||
|
>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar
|
||||||
|
src={(session().account as Account).inf?.avatar}
|
||||||
|
></Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
secondary={
|
||||||
|
relationship()?.following
|
||||||
|
? undefined
|
||||||
|
: profile()?.locked
|
||||||
|
? "A request will be sent"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span innerHTML={sessionDisplayName()}></span>
|
||||||
|
<span>'s Home</span>
|
||||||
|
</ListItemText>
|
||||||
|
|
||||||
|
<Checkbox checked={relationship()?.following ?? false} />
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="intro"
|
||||||
|
style={{
|
||||||
|
"background-color": bannerSampledColors()?.average,
|
||||||
|
color: bannerSampledColors()?.text,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
<section class="acct-grp">
|
||||||
aria-label="Load More"
|
<Avatar
|
||||||
aria-controls={recentTootListId}
|
src={avatarImg()}
|
||||||
size="large"
|
alt={`${profile()?.displayName || "the user"}'s avatar`}
|
||||||
color="primary"
|
sx={{
|
||||||
onClick={[refetchRecentToots, "prev"]}
|
marginTop: "calc(-16px - 72px / 2)",
|
||||||
disabled={isTootListLoading()}
|
width: "72px",
|
||||||
|
height: "72px",
|
||||||
|
}}
|
||||||
|
></Avatar>
|
||||||
|
<div class="name-grp">
|
||||||
|
<div class="display-name">
|
||||||
|
<Show when={profile()?.bot}>
|
||||||
|
<SmartToySharp class="acct-mark" aria-label="Bot" />
|
||||||
|
</Show>
|
||||||
|
<Show when={profile()?.locked}>
|
||||||
|
<Lock class="acct-mark" aria-label="Locked" />
|
||||||
|
</Show>
|
||||||
|
<Body2
|
||||||
|
component="span"
|
||||||
|
innerHTML={displayName()}
|
||||||
|
aria-label="Display name"
|
||||||
|
></Body2>
|
||||||
|
</div>
|
||||||
|
<span aria-label="Complete username" class="username">
|
||||||
|
{fullUsername()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div role="presentation">
|
||||||
|
<Switch>
|
||||||
|
<Match
|
||||||
|
when={
|
||||||
|
!session().account ||
|
||||||
|
profileUncaught.loading ||
|
||||||
|
profileUncaught.error
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{<></>}
|
||||||
|
</Match>
|
||||||
|
<Match when={isCurrentSessionProfile()}>
|
||||||
|
<IconButton color="inherit">
|
||||||
|
<Edit />
|
||||||
|
</IconButton>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={(event) => {
|
||||||
|
openSubscribeMenu(
|
||||||
|
event.currentTarget.getBoundingClientRect(),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{relationship()?.following ? "Subscribed" : "Subscribe"}
|
||||||
|
</Button>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
class="description"
|
||||||
|
aria-label={`${profile()?.displayName || "the user"}'s description`}
|
||||||
|
innerHTML={description() || ""}
|
||||||
|
></section>
|
||||||
|
|
||||||
|
<table
|
||||||
|
class="acct-fields"
|
||||||
|
aria-label={`${profile()?.displayName || "the user"}'s fields`}
|
||||||
>
|
>
|
||||||
<Show when={isTootListLoading()} fallback={<ExpandMore />}>
|
<tbody>
|
||||||
<CircularProgress sx={{ width: "24px", height: "24px" }} />
|
<For each={profile()?.fields ?? []}>
|
||||||
</Show>
|
{(item, index) => {
|
||||||
</IconButton>
|
return (
|
||||||
|
<tr data-field-index={index()}>
|
||||||
|
<td>{item.name}</td>
|
||||||
|
<td>
|
||||||
|
<Show when={item.verifiedAt}>
|
||||||
|
<Verified />
|
||||||
|
</Show>
|
||||||
|
</td>
|
||||||
|
<td innerHTML={item.value}></td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
|
||||||
</Scaffold>
|
<div class="recent-toots" role="presentation">
|
||||||
|
<div class="toot-list-toolbar">
|
||||||
|
<TootFilterButton
|
||||||
|
options={{
|
||||||
|
pinned: "Pinneds",
|
||||||
|
boost: "Boosts",
|
||||||
|
reply: "Replies",
|
||||||
|
original: "Originals",
|
||||||
|
}}
|
||||||
|
applied={recentTootFilter()}
|
||||||
|
onApply={setRecentTootFilter}
|
||||||
|
disabledKeys={["original"]}
|
||||||
|
></TootFilterButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ItemSelectionProvider value={selectionState}>
|
||||||
|
<TimeSourceProvider value={time}>
|
||||||
|
<Show
|
||||||
|
when={recentTootFilter().pinned && pinnedToots.list.length > 0}
|
||||||
|
>
|
||||||
|
<TootList
|
||||||
|
threads={pinnedToots.list}
|
||||||
|
onUnknownThread={pinnedToots.getPath}
|
||||||
|
onChangeToot={pinnedToots.set}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
</Show>
|
||||||
|
<TootList
|
||||||
|
id={recentTootListId}
|
||||||
|
threads={recentToots.list}
|
||||||
|
onUnknownThread={recentToots.getPath}
|
||||||
|
onChangeToot={recentToots.set}
|
||||||
|
/>
|
||||||
|
</TimeSourceProvider>
|
||||||
|
</ItemSelectionProvider>
|
||||||
|
|
||||||
|
<Show when={!recentTootChunk()?.done}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
"text-align": "center",
|
||||||
|
"padding-bottom": "var(--safe-area-inset-bottom)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Load More"
|
||||||
|
aria-controls={recentTootListId}
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
onClick={[refetchRecentToots, "prev"]}
|
||||||
|
disabled={isTootListLoading()}
|
||||||
|
>
|
||||||
|
<Show when={isTootListLoading()} fallback={<ExpandMore />}>
|
||||||
|
<CircularProgress sx={{ width: "24px", height: "24px" }} />
|
||||||
|
</Show>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Scaffold>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { useStore } from "@nanostores/solid";
|
||||||
import { $settings } from "./stores";
|
import { $settings } from "./stores";
|
||||||
import { useNavigator } from "~platform/StackedRouter";
|
import { useNavigator } from "~platform/StackedRouter";
|
||||||
import AppTopBar from "~material/AppTopBar";
|
import AppTopBar from "~material/AppTopBar";
|
||||||
|
import DocumentTitle from "~platform/DocumentTitle";
|
||||||
|
|
||||||
const ChooseLang: Component = () => {
|
const ChooseLang: Component = () => {
|
||||||
const { pop } = useNavigator();
|
const { pop } = useNavigator();
|
||||||
|
@ -53,67 +54,70 @@ const ChooseLang: Component = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Scaffold
|
<>
|
||||||
topbar={
|
<DocumentTitle>{t("Choose Language")}</DocumentTitle>
|
||||||
<AppTopBar>
|
<Scaffold
|
||||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
topbar={
|
||||||
<ArrowBack />
|
<AppTopBar>
|
||||||
</IconButton>
|
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||||
<Title>{t("Choose Language")}</Title>
|
<ArrowBack />
|
||||||
</AppTopBar>
|
</IconButton>
|
||||||
}
|
<Title>{t("Choose Language")}</Title>
|
||||||
>
|
</AppTopBar>
|
||||||
<List
|
}
|
||||||
sx={{
|
|
||||||
paddingBottom: "var(--safe-area-inset-bottom, 0)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ListItemButton
|
<List
|
||||||
onClick={() => {
|
sx={{
|
||||||
onCodeChange(code() ? undefined : matchedLangCode());
|
paddingBottom: "var(--safe-area-inset-bottom, 0)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemText>
|
<ListItemButton
|
||||||
{t("lang.auto", {
|
onClick={() => {
|
||||||
detected: t(`lang.${matchedLangCode()}`) ?? matchedLangCode(),
|
onCodeChange(code() ? undefined : matchedLangCode());
|
||||||
})}
|
}}
|
||||||
</ListItemText>
|
>
|
||||||
<ListItemSecondaryAction>
|
<ListItemText>
|
||||||
<Switch checked={typeof code() === "undefined"} />
|
{t("lang.auto", {
|
||||||
</ListItemSecondaryAction>
|
detected: t(`lang.${matchedLangCode()}`) ?? matchedLangCode(),
|
||||||
</ListItemButton>
|
})}
|
||||||
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
|
</ListItemText>
|
||||||
<For each={SUPPORTED_LANGS}>
|
<ListItemSecondaryAction>
|
||||||
{(c) => (
|
<Switch checked={typeof code() === "undefined"} />
|
||||||
<ListItemButton
|
</ListItemSecondaryAction>
|
||||||
disabled={typeof code() === "undefined"}
|
</ListItemButton>
|
||||||
onClick={[onCodeChange, c]}
|
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
|
||||||
>
|
<For each={SUPPORTED_LANGS}>
|
||||||
<ListItemText>{t(`lang.${c}`)}</ListItemText>
|
{(c) => (
|
||||||
<ListItemSecondaryAction>
|
<ListItemButton
|
||||||
<Radio
|
disabled={typeof code() === "undefined"}
|
||||||
checked={
|
onClick={[onCodeChange, c]}
|
||||||
code() === c ||
|
>
|
||||||
(code() === undefined && matchedLangCode() == c)
|
<ListItemText>{t(`lang.${c}`)}</ListItemText>
|
||||||
}
|
<ListItemSecondaryAction>
|
||||||
/>
|
<Radio
|
||||||
</ListItemSecondaryAction>
|
checked={
|
||||||
</ListItemButton>
|
code() === c ||
|
||||||
)}
|
(code() === undefined && matchedLangCode() == c)
|
||||||
</For>
|
}
|
||||||
</List>
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItemButton>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</List>
|
||||||
|
|
||||||
<List subheader={<ListSubheader>{t("Unsupported")}</ListSubheader>}>
|
<List subheader={<ListSubheader>{t("Unsupported")}</ListSubheader>}>
|
||||||
<For each={unsupportedLangCodes()}>
|
<For each={unsupportedLangCodes()}>
|
||||||
{(code) => (
|
{(code) => (
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText>{iso639_1.getNativeName(code)}</ListItemText>
|
<ListItemText>{iso639_1.getNativeName(code)}</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
</List>
|
||||||
</List>
|
</List>
|
||||||
</List>
|
</Scaffold>
|
||||||
</Scaffold>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,10 @@ import { useStore } from "@nanostores/solid";
|
||||||
import { $settings } from "./stores";
|
import { $settings } from "./stores";
|
||||||
import { useNavigator } from "~platform/StackedRouter";
|
import { useNavigator } from "~platform/StackedRouter";
|
||||||
import AppTopBar from "~material/AppTopBar";
|
import AppTopBar from "~material/AppTopBar";
|
||||||
|
import DocumentTitle from "~platform/DocumentTitle";
|
||||||
|
|
||||||
const Motions: Component = () => {
|
const Motions: Component = () => {
|
||||||
const {pop} = useNavigator();
|
const { pop } = useNavigator();
|
||||||
const [t] = createTranslator(
|
const [t] = createTranslator(
|
||||||
(code) =>
|
(code) =>
|
||||||
import(`./i18n/${code}.json`) as Promise<{
|
import(`./i18n/${code}.json`) as Promise<{
|
||||||
|
@ -30,55 +31,58 @@ const Motions: Component = () => {
|
||||||
);
|
);
|
||||||
const settings = useStore($settings);
|
const settings = useStore($settings);
|
||||||
return (
|
return (
|
||||||
<Scaffold
|
<>
|
||||||
topbar={
|
<DocumentTitle>{t("motions")}</DocumentTitle>
|
||||||
<AppTopBar>
|
<Scaffold
|
||||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
topbar={
|
||||||
|
<AppTopBar>
|
||||||
|
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||||
<ArrowBack />
|
<ArrowBack />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Title>{t("motions")}</Title>
|
<Title>{t("motions")}</Title>
|
||||||
</AppTopBar>
|
</AppTopBar>
|
||||||
}
|
}
|
||||||
>
|
|
||||||
<List
|
|
||||||
sx={{
|
|
||||||
paddingBottom: "calc(var(--safe-area-inset-bottom, 0px) + 16px)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<li>
|
<List
|
||||||
<ul style={{ "padding-left": 0 }}>
|
sx={{
|
||||||
<ListSubheader>{t("motions.gifs")}</ListSubheader>
|
paddingBottom: "calc(var(--safe-area-inset-bottom, 0px) + 16px)",
|
||||||
<ListItemButton
|
}}
|
||||||
onClick={() =>
|
>
|
||||||
$settings.setKey("autoPlayGIFs", !settings().autoPlayGIFs)
|
<li>
|
||||||
}
|
<ul style={{ "padding-left": 0 }}>
|
||||||
>
|
<ListSubheader>{t("motions.gifs")}</ListSubheader>
|
||||||
<ListItemText>{t("motions.gifs.autoplay")}</ListItemText>
|
<ListItemButton
|
||||||
<ListItemSecondaryAction>
|
onClick={() =>
|
||||||
<Switch checked={settings().autoPlayGIFs}></Switch>
|
$settings.setKey("autoPlayGIFs", !settings().autoPlayGIFs)
|
||||||
</ListItemSecondaryAction>
|
}
|
||||||
</ListItemButton>
|
>
|
||||||
<Divider />
|
<ListItemText>{t("motions.gifs.autoplay")}</ListItemText>
|
||||||
</ul>
|
<ListItemSecondaryAction>
|
||||||
</li>
|
<Switch checked={settings().autoPlayGIFs}></Switch>
|
||||||
<li>
|
</ListItemSecondaryAction>
|
||||||
<ul style={{ "padding-left": 0 }}>
|
</ListItemButton>
|
||||||
<ListSubheader>{t("motions.vids")}</ListSubheader>
|
<Divider />
|
||||||
<ListItemButton
|
</ul>
|
||||||
onClick={() =>
|
</li>
|
||||||
$settings.setKey("autoPlayVideos", !settings().autoPlayVideos)
|
<li>
|
||||||
}
|
<ul style={{ "padding-left": 0 }}>
|
||||||
>
|
<ListSubheader>{t("motions.vids")}</ListSubheader>
|
||||||
<ListItemText>{t("motions.vids.autoplay")}</ListItemText>
|
<ListItemButton
|
||||||
<ListItemSecondaryAction>
|
onClick={() =>
|
||||||
<Switch checked={settings().autoPlayVideos}></Switch>
|
$settings.setKey("autoPlayVideos", !settings().autoPlayVideos)
|
||||||
</ListItemSecondaryAction>
|
}
|
||||||
</ListItemButton>
|
>
|
||||||
<Divider />
|
<ListItemText>{t("motions.vids.autoplay")}</ListItemText>
|
||||||
</ul>
|
<ListItemSecondaryAction>
|
||||||
</li>
|
<Switch checked={settings().autoPlayVideos}></Switch>
|
||||||
</List>
|
</ListItemSecondaryAction>
|
||||||
</Scaffold>
|
</ListItemButton>
|
||||||
|
<Divider />
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</List>
|
||||||
|
</Scaffold>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { $settings } from "./stores";
|
||||||
import { useStore } from "@nanostores/solid";
|
import { useStore } from "@nanostores/solid";
|
||||||
import { useNavigator } from "~platform/StackedRouter";
|
import { useNavigator } from "~platform/StackedRouter";
|
||||||
import AppTopBar from "~material/AppTopBar";
|
import AppTopBar from "~material/AppTopBar";
|
||||||
|
import DocumentTitle from "~platform/DocumentTitle";
|
||||||
|
|
||||||
const ChooseRegion: Component = () => {
|
const ChooseRegion: Component = () => {
|
||||||
const { pop } = useNavigator();
|
const { pop } = useNavigator();
|
||||||
|
@ -48,59 +49,62 @@ const ChooseRegion: Component = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Scaffold
|
<>
|
||||||
topbar={
|
<DocumentTitle>{t("Choose Region")}</DocumentTitle>
|
||||||
<AppTopBar>
|
<Scaffold
|
||||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
topbar={
|
||||||
<ArrowBack />
|
<AppTopBar>
|
||||||
</IconButton>
|
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||||
<Title>{t("Choose Region")}</Title>
|
<ArrowBack />
|
||||||
</AppTopBar>
|
</IconButton>
|
||||||
}
|
<Title>{t("Choose Region")}</Title>
|
||||||
>
|
</AppTopBar>
|
||||||
<List
|
}
|
||||||
sx={{
|
|
||||||
paddingBottom: "var(--safe-area-inset-bottom, 0)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ListItemButton
|
<List
|
||||||
onClick={() => {
|
sx={{
|
||||||
onCodeChange(region() ? undefined : matchedRegionCode());
|
paddingBottom: "var(--safe-area-inset-bottom, 0)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemText>
|
<ListItemButton
|
||||||
{t("region.auto", {
|
onClick={() => {
|
||||||
detected:
|
onCodeChange(region() ? undefined : matchedRegionCode());
|
||||||
t(`region.${matchedRegionCode()}`) ?? matchedRegionCode(),
|
}}
|
||||||
})}
|
>
|
||||||
</ListItemText>
|
<ListItemText>
|
||||||
<ListItemSecondaryAction>
|
{t("region.auto", {
|
||||||
<Switch checked={typeof region() === "undefined"} />
|
detected:
|
||||||
</ListItemSecondaryAction>
|
t(`region.${matchedRegionCode()}`) ?? matchedRegionCode(),
|
||||||
</ListItemButton>
|
})}
|
||||||
|
</ListItemText>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch checked={typeof region() === "undefined"} />
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItemButton>
|
||||||
|
|
||||||
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
|
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
|
||||||
<For each={SUPPORTED_REGIONS}>
|
<For each={SUPPORTED_REGIONS}>
|
||||||
{(code) => (
|
{(code) => (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
disabled={typeof region() === "undefined"}
|
disabled={typeof region() === "undefined"}
|
||||||
onClick={[onCodeChange, code]}
|
onClick={[onCodeChange, code]}
|
||||||
>
|
>
|
||||||
<ListItemText>{t(`region.${code}`)}</ListItemText>
|
<ListItemText>{t(`region.${code}`)}</ListItemText>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Radio
|
<Radio
|
||||||
checked={
|
checked={
|
||||||
region() === code ||
|
region() === code ||
|
||||||
(region() === undefined && matchedRegionCode() == code)
|
(region() === undefined && matchedRegionCode() == code)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
</List>
|
||||||
</List>
|
</List>
|
||||||
</List>
|
</Scaffold>
|
||||||
</Scaffold>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ import { makeAcctText, useSessions } from "../masto/clients.js";
|
||||||
import { useNavigator } from "~platform/StackedRouter.jsx";
|
import { useNavigator } from "~platform/StackedRouter.jsx";
|
||||||
import AppTopBar from "~material/AppTopBar.jsx";
|
import AppTopBar from "~material/AppTopBar.jsx";
|
||||||
import MastodonLogo from "./MastodonLogo.jsx";
|
import MastodonLogo from "./MastodonLogo.jsx";
|
||||||
|
import DocumentTitle from "~platform/DocumentTitle.jsx";
|
||||||
|
|
||||||
type Inset = {
|
type Inset = {
|
||||||
top?: number;
|
top?: number;
|
||||||
|
@ -197,225 +198,228 @@ const Settings: Component = () => {
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
return (
|
return (
|
||||||
<Scaffold
|
<>
|
||||||
topbar={
|
<DocumentTitle>{t("Settings")}</DocumentTitle>
|
||||||
<AppTopBar>
|
<Scaffold
|
||||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
topbar={
|
||||||
<CloseIcon />
|
<AppTopBar>
|
||||||
</IconButton>
|
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||||
<Title>{t("Settings")}</Title>
|
<CloseIcon />
|
||||||
</AppTopBar>
|
</IconButton>
|
||||||
}
|
<Title>{t("Settings")}</Title>
|
||||||
>
|
</AppTopBar>
|
||||||
<List class="setting-list" use:solid-styled>
|
}
|
||||||
<li>
|
>
|
||||||
<ul>
|
<List class="setting-list" use:solid-styled>
|
||||||
<ListSubheader>{t("Accounts")}</ListSubheader>
|
|
||||||
<ListItemButton disabled>
|
|
||||||
<ListItemText>{t("All Notifications")}</ListItemText>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<Switch value={false} disabled />
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItemButton>
|
|
||||||
<Divider />
|
|
||||||
<ListItemButton disabled>
|
|
||||||
<ListItemText>{t("Sign in...")}</ListItemText>
|
|
||||||
</ListItemButton>
|
|
||||||
<Divider />
|
|
||||||
</ul>
|
|
||||||
<For each={profiles()}>
|
|
||||||
{({ account: acct }) => (
|
|
||||||
<ul data-site={acct.site} data-username={acct.inf?.username}>
|
|
||||||
<ListSubheader>{`@${acct.inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader>
|
|
||||||
<ListItemButton disabled>
|
|
||||||
<ListItemText>{t("Notifications")}</ListItemText>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<Switch value={false} disabled />
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItemButton>
|
|
||||||
<Divider />
|
|
||||||
<ListItemButton onClick={[doSignOut, acct]}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Logout />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>{t("Sign out")}</ListItemText>
|
|
||||||
</ListItemButton>
|
|
||||||
<Divider />
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<ListSubheader>{t("timelines")}</ListSubheader>
|
|
||||||
<ListItemButton
|
|
||||||
onClick={(e) =>
|
|
||||||
$settings.setKey(
|
|
||||||
"prefetchTootsDisabled",
|
|
||||||
!settings$().prefetchTootsDisabled,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ListItemText secondary={t("Prefetch Toots.2nd")}>
|
|
||||||
{t("Prefetch Toots")}
|
|
||||||
</ListItemText>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<Switch checked={!settings$().prefetchTootsDisabled} />
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItemButton>
|
|
||||||
<Divider />
|
|
||||||
<ListItemButton component={A} href="./motions">
|
|
||||||
<ListItemIcon>
|
|
||||||
<AnimationIcon></AnimationIcon>
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>{t("motions")}</ListItemText>
|
|
||||||
</ListItemButton>
|
|
||||||
<Divider />
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<ListSubheader>{t("storage")}</ListSubheader>
|
|
||||||
<ListItemButton disabled>
|
|
||||||
<ListItemIcon>
|
|
||||||
<DeleteForever />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText secondary={t("storage.cache.UA-managed")}>
|
|
||||||
{t("storage.cache.clear")}
|
|
||||||
</ListItemText>
|
|
||||||
</ListItemButton>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<ListSubheader>{t("This Application")}</ListSubheader>
|
|
||||||
<ListItemButton component={A} href="./language">
|
|
||||||
<ListItemIcon>
|
|
||||||
<TranslateIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText
|
|
||||||
secondary={
|
|
||||||
settings$().language === undefined
|
|
||||||
? t("lang.auto", {
|
|
||||||
detected:
|
|
||||||
t("lang." + autoMatchLangTag()) ?? autoMatchLangTag(),
|
|
||||||
})
|
|
||||||
: t("lang." + settings$().language)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("Language")}
|
|
||||||
</ListItemText>
|
|
||||||
</ListItemButton>
|
|
||||||
<Divider />
|
|
||||||
<ListItemButton component={A} href="./region">
|
|
||||||
<ListItemIcon>
|
|
||||||
<PublicIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText
|
|
||||||
secondary={
|
|
||||||
settings$().region === undefined
|
|
||||||
? t("region.auto", {
|
|
||||||
detected:
|
|
||||||
t("region." + autoMatchRegion()) ?? autoMatchRegion(),
|
|
||||||
})
|
|
||||||
: t("region." + settings$().region)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("Region")}
|
|
||||||
</ListItemText>
|
|
||||||
</ListItemButton>
|
|
||||||
<Divider />
|
|
||||||
<ListItem
|
|
||||||
secondaryAction={
|
|
||||||
<IconButton
|
|
||||||
component={A}
|
|
||||||
aria-label={t("mastodonlink.open")}
|
|
||||||
href={`/${encodeURIComponent(profiles().length > 0 ? makeAcctText(profiles()[0]) : "@")}/profile/@tutu@indieweb.social`}
|
|
||||||
>
|
|
||||||
<MastodonLogo />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ListItemText secondary={t("About Tutu.2nd")}>
|
|
||||||
{t("About Tutu")}
|
|
||||||
</ListItemText>
|
|
||||||
</ListItem>
|
|
||||||
<Divider />
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText
|
|
||||||
secondary={t("version", {
|
|
||||||
packageVersion: import.meta.env.PACKAGE_VERSION,
|
|
||||||
builtAt: format(
|
|
||||||
import.meta.env.BUILT_AT,
|
|
||||||
t("datefmt") || "yyyy/MM/dd",
|
|
||||||
{ locale: dateFnLocale() },
|
|
||||||
),
|
|
||||||
buildMode: import.meta.env.MODE,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{needRefresh() ? t("updates.ready") : t("updates.no")}
|
|
||||||
</ListItemText>
|
|
||||||
<Show when={needRefresh()}>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<IconButton
|
|
||||||
aria-label="Restart Now"
|
|
||||||
onClick={() => window.location.reload()}
|
|
||||||
>
|
|
||||||
<RefreshIcon />
|
|
||||||
</IconButton>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</Show>
|
|
||||||
</ListItem>
|
|
||||||
<Divider />
|
|
||||||
{import.meta.env.VITE_CODE_VERSION ? (
|
|
||||||
<>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText secondary={import.meta.env.VITE_CODE_VERSION}>
|
|
||||||
{t("version.code")}
|
|
||||||
</ListItemText>
|
|
||||||
</ListItem>
|
|
||||||
<Divider />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
{import.meta.env.DEV ? (
|
|
||||||
<li>
|
<li>
|
||||||
<ListSubheader>Developer Tools</ListSubheader>
|
<ul>
|
||||||
<ListItem
|
<ListSubheader>{t("Accounts")}</ListSubheader>
|
||||||
secondaryAction={
|
<ListItemButton disabled>
|
||||||
window.screen?.orientation ? (
|
<ListItemText>{t("All Notifications")}</ListItemText>
|
||||||
<NativeSelect
|
<ListItemSecondaryAction>
|
||||||
sx={{ maxWidth: "40vw" }}
|
<Switch value={false} disabled />
|
||||||
onChange={(event) => {
|
</ListItemSecondaryAction>
|
||||||
const k = event.currentTarget.value;
|
</ListItemButton>
|
||||||
setupSafeAreaEmulation(k);
|
<Divider />
|
||||||
}}
|
<ListItemButton disabled>
|
||||||
>
|
<ListItemText>{t("Sign in...")}</ListItemText>
|
||||||
<option>Don't change</option>
|
</ListItemButton>
|
||||||
<option value={"ua"}>User agent</option>
|
<Divider />
|
||||||
<option value={"iphone15"}>
|
</ul>
|
||||||
iPhone 15 and Plus, Pro, Pro Max
|
<For each={profiles()}>
|
||||||
</option>
|
{({ account: acct }) => (
|
||||||
<option value={"iphone12"}>iPhone 12, 13 and 14</option>
|
<ul data-site={acct.site} data-username={acct.inf?.username}>
|
||||||
<option value={"iphone13mini"}>iPhone 13 mini</option>
|
<ListSubheader>{`@${acct.inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader>
|
||||||
</NativeSelect>
|
<ListItemButton disabled>
|
||||||
) : undefined
|
<ListItemText>{t("Notifications")}</ListItemText>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch value={false} disabled />
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItemButton>
|
||||||
|
<Divider />
|
||||||
|
<ListItemButton onClick={[doSignOut, acct]}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Logout />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>{t("Sign out")}</ListItemText>
|
||||||
|
</ListItemButton>
|
||||||
|
<Divider />
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ListSubheader>{t("timelines")}</ListSubheader>
|
||||||
|
<ListItemButton
|
||||||
|
onClick={(e) =>
|
||||||
|
$settings.setKey(
|
||||||
|
"prefetchTootsDisabled",
|
||||||
|
!settings$().prefetchTootsDisabled,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<ListItemText secondary={t("Prefetch Toots.2nd")}>
|
||||||
|
{t("Prefetch Toots")}
|
||||||
|
</ListItemText>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch checked={!settings$().prefetchTootsDisabled} />
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItemButton>
|
||||||
|
<Divider />
|
||||||
|
<ListItemButton component={A} href="./motions">
|
||||||
|
<ListItemIcon>
|
||||||
|
<AnimationIcon></AnimationIcon>
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>{t("motions")}</ListItemText>
|
||||||
|
</ListItemButton>
|
||||||
|
<Divider />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ListSubheader>{t("storage")}</ListSubheader>
|
||||||
|
<ListItemButton disabled>
|
||||||
|
<ListItemIcon>
|
||||||
|
<DeleteForever />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText secondary={t("storage.cache.UA-managed")}>
|
||||||
|
{t("storage.cache.clear")}
|
||||||
|
</ListItemText>
|
||||||
|
</ListItemButton>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ListSubheader>{t("This Application")}</ListSubheader>
|
||||||
|
<ListItemButton component={A} href="./language">
|
||||||
|
<ListItemIcon>
|
||||||
|
<TranslateIcon />
|
||||||
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
secondary={
|
secondary={
|
||||||
window.screen?.orientation
|
settings$().language === undefined
|
||||||
? undefined
|
? t("lang.auto", {
|
||||||
: "Unsupported on This Platform"
|
detected:
|
||||||
|
t("lang." + autoMatchLangTag()) ?? autoMatchLangTag(),
|
||||||
|
})
|
||||||
|
: t("lang." + settings$().language)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Safe Area Insets
|
{t("Language")}
|
||||||
|
</ListItemText>
|
||||||
|
</ListItemButton>
|
||||||
|
<Divider />
|
||||||
|
<ListItemButton component={A} href="./region">
|
||||||
|
<ListItemIcon>
|
||||||
|
<PublicIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
secondary={
|
||||||
|
settings$().region === undefined
|
||||||
|
? t("region.auto", {
|
||||||
|
detected:
|
||||||
|
t("region." + autoMatchRegion()) ?? autoMatchRegion(),
|
||||||
|
})
|
||||||
|
: t("region." + settings$().region)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("Region")}
|
||||||
|
</ListItemText>
|
||||||
|
</ListItemButton>
|
||||||
|
<Divider />
|
||||||
|
<ListItem
|
||||||
|
secondaryAction={
|
||||||
|
<IconButton
|
||||||
|
component={A}
|
||||||
|
aria-label={t("mastodonlink.open")}
|
||||||
|
href={`/${encodeURIComponent(profiles().length > 0 ? makeAcctText(profiles()[0]) : "@")}/profile/@tutu@indieweb.social`}
|
||||||
|
>
|
||||||
|
<MastodonLogo />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItemText secondary={t("About Tutu.2nd")}>
|
||||||
|
{t("About Tutu")}
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
secondary={t("version", {
|
||||||
|
packageVersion: import.meta.env.PACKAGE_VERSION,
|
||||||
|
builtAt: format(
|
||||||
|
import.meta.env.BUILT_AT,
|
||||||
|
t("datefmt") || "yyyy/MM/dd",
|
||||||
|
{ locale: dateFnLocale() },
|
||||||
|
),
|
||||||
|
buildMode: import.meta.env.MODE,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{needRefresh() ? t("updates.ready") : t("updates.no")}
|
||||||
|
</ListItemText>
|
||||||
|
<Show when={needRefresh()}>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Restart Now"
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
>
|
||||||
|
<RefreshIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</Show>
|
||||||
|
</ListItem>
|
||||||
|
<Divider />
|
||||||
|
{import.meta.env.VITE_CODE_VERSION ? (
|
||||||
|
<>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText secondary={import.meta.env.VITE_CODE_VERSION}>
|
||||||
|
{t("version.code")}
|
||||||
|
</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
) : (
|
{import.meta.env.DEV ? (
|
||||||
<></>
|
<li>
|
||||||
)}
|
<ListSubheader>Developer Tools</ListSubheader>
|
||||||
</List>
|
<ListItem
|
||||||
</Scaffold>
|
secondaryAction={
|
||||||
|
window.screen?.orientation ? (
|
||||||
|
<NativeSelect
|
||||||
|
sx={{ maxWidth: "40vw" }}
|
||||||
|
onChange={(event) => {
|
||||||
|
const k = event.currentTarget.value;
|
||||||
|
setupSafeAreaEmulation(k);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option>Don't change</option>
|
||||||
|
<option value={"ua"}>User agent</option>
|
||||||
|
<option value={"iphone15"}>
|
||||||
|
iPhone 15 and Plus, Pro, Pro Max
|
||||||
|
</option>
|
||||||
|
<option value={"iphone12"}>iPhone 12, 13 and 14</option>
|
||||||
|
<option value={"iphone13mini"}>iPhone 13 mini</option>
|
||||||
|
</NativeSelect>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
secondary={
|
||||||
|
window.screen?.orientation
|
||||||
|
? undefined
|
||||||
|
: "Unsupported on This Platform"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Safe Area Insets
|
||||||
|
</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
<Divider />
|
||||||
|
</li>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Scaffold>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {
|
||||||
createEffect,
|
createEffect,
|
||||||
useTransition,
|
useTransition,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { useDocumentTitle } from "../utils";
|
|
||||||
import Scaffold from "~material/Scaffold";
|
import Scaffold from "~material/Scaffold";
|
||||||
import {
|
import {
|
||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
|
@ -30,6 +29,7 @@ import {
|
||||||
import AppTopBar from "~material/AppTopBar";
|
import AppTopBar from "~material/AppTopBar";
|
||||||
import { createTranslator } from "~platform/i18n";
|
import { createTranslator } from "~platform/i18n";
|
||||||
import { useWindowSize } from "@solid-primitives/resize-observer";
|
import { useWindowSize } from "@solid-primitives/resize-observer";
|
||||||
|
import DocumentTitle from "~platform/DocumentTitle";
|
||||||
|
|
||||||
type StringRes = Record<
|
type StringRes = Record<
|
||||||
"tabs.home" | "tabs.trending" | "tabs.public" | "set.prefetch-toots",
|
"tabs.home" | "tabs.trending" | "tabs.public" | "set.prefetch-toots",
|
||||||
|
@ -38,7 +38,6 @@ type StringRes = Record<
|
||||||
|
|
||||||
const Home: ParentComponent = (props) => {
|
const Home: ParentComponent = (props) => {
|
||||||
let panelList: HTMLDivElement;
|
let panelList: HTMLDivElement;
|
||||||
useDocumentTitle("Timelines");
|
|
||||||
const [t] = createTranslator(
|
const [t] = createTranslator(
|
||||||
(code) => import(`./i18n/${code}.json`) as Promise<{ default: StringRes }>,
|
(code) => import(`./i18n/${code}.json`) as Promise<{ default: StringRes }>,
|
||||||
);
|
);
|
||||||
|
@ -179,6 +178,7 @@ const Home: ParentComponent = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<DocumentTitle>Timelines</DocumentTitle>
|
||||||
<Scaffold
|
<Scaffold
|
||||||
topbar={
|
topbar={
|
||||||
<AppTopBar>
|
<AppTopBar>
|
||||||
|
|
|
@ -14,7 +14,6 @@ import cards from "~material/cards.module.css";
|
||||||
import { css } from "solid-styled";
|
import { css } from "solid-styled";
|
||||||
import { createTimeSource, TimeSourceProvider } from "~platform/timesrc";
|
import { createTimeSource, TimeSourceProvider } from "~platform/timesrc";
|
||||||
import TootComposer from "./TootComposer";
|
import TootComposer from "./TootComposer";
|
||||||
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";
|
||||||
|
@ -26,6 +25,7 @@ import ItemSelectionProvider, {
|
||||||
import AppTopBar from "~material/AppTopBar";
|
import AppTopBar from "~material/AppTopBar";
|
||||||
import { fetchStatus } from "../masto/statuses";
|
import { fetchStatus } from "../masto/statuses";
|
||||||
import { type Account } from "../accounts/stores";
|
import { type Account } from "../accounts/stores";
|
||||||
|
import DocumentTitle from "~platform/DocumentTitle";
|
||||||
|
|
||||||
const TootBottomSheet: Component = (props) => {
|
const TootBottomSheet: Component = (props) => {
|
||||||
const params = useParams<{ acct: string; id: string }>();
|
const params = useParams<{ acct: string; id: string }>();
|
||||||
|
@ -65,11 +65,11 @@ const TootBottomSheet: Component = (props) => {
|
||||||
() => tootContext()?.descendants,
|
() => tootContext()?.descendants,
|
||||||
);
|
);
|
||||||
|
|
||||||
useDocumentTitle(() => {
|
const documentTitle = () => {
|
||||||
const t = toot()?.reblog ?? toot();
|
const t = toot()?.reblog ?? toot();
|
||||||
const name = t?.account.displayName ?? "Someone";
|
const name = t?.account.displayName ?? "Someone";
|
||||||
return `${name}'s toot`;
|
return `${name}'s toot`;
|
||||||
});
|
};
|
||||||
|
|
||||||
const tootDisplayName = () => {
|
const tootDisplayName = () => {
|
||||||
const t = toot()?.reblog ?? toot();
|
const t = toot()?.reblog ?? toot();
|
||||||
|
@ -163,6 +163,7 @@ const TootBottomSheet: Component = (props) => {
|
||||||
}
|
}
|
||||||
class="TootBottomSheet"
|
class="TootBottomSheet"
|
||||||
>
|
>
|
||||||
|
<DocumentTitle>{documentTitle()}</DocumentTitle>
|
||||||
<div class="Scrollable">
|
<div class="Scrollable">
|
||||||
<TimeSourceProvider value={time}>
|
<TimeSourceProvider value={time}>
|
||||||
<ItemSelectionProvider value={selectionState}>
|
<ItemSelectionProvider value={selectionState}>
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
import {
|
|
||||||
createRenderEffect,
|
|
||||||
onCleanup,
|
|
||||||
type Accessor,
|
|
||||||
} from "solid-js";
|
|
||||||
|
|
||||||
export function useDocumentTitle(newTitle?: string | Accessor<string>) {
|
|
||||||
const capturedTitle = document.title;
|
|
||||||
|
|
||||||
createRenderEffect(() => {
|
|
||||||
if (newTitle)
|
|
||||||
document.title = typeof newTitle === "string" ? newTitle : newTitle();
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
document.title = capturedTitle;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (x: ((x: string) => string) | string) =>
|
|
||||||
(document.title = typeof x === "string" ? x : x(document.title));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mergeClass(c1: string | undefined, c2: string | undefined) {
|
|
||||||
if (!c1) {
|
|
||||||
return c2;
|
|
||||||
}
|
|
||||||
if (!c2) {
|
|
||||||
return c1;
|
|
||||||
}
|
|
||||||
return [c1, c2].join(" ");
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue