remove utils
All checks were successful
/ depoly (push) Successful in 1m22s

* add ~platform/DocumentTitle
* add titles for some pages
This commit is contained in:
thislight 2025-01-02 22:43:37 +08:00
parent 1115135380
commit 1c8a3f0bbb
No known key found for this signature in database
GPG key ID: FCFE5192241CCD4E
12 changed files with 843 additions and 823 deletions

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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