* 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";
|
||||
import { acceptAccountViaAuthCode } from "./stores";
|
||||
import { $settings } from "../settings/stores";
|
||||
import { useDocumentTitle } from "../utils";
|
||||
import cards from "~material/cards.module.css";
|
||||
import { LinearProgress } from "@suid/material";
|
||||
import Img from "~material/Img";
|
||||
import { createRestAPIClient } from "masto";
|
||||
import { Title } from "~material/typography";
|
||||
import { useNavigator } from "~platform/StackedRouter";
|
||||
import DocumentTitle from "~platform/DocumentTitle";
|
||||
|
||||
type OAuth2CallbackParams = {
|
||||
code?: string;
|
||||
|
@ -27,13 +27,12 @@ const MastodonOAuth2Callback: Component = () => {
|
|||
const titleId = createUniqueId();
|
||||
const [params] = useSearchParams<OAuth2CallbackParams>();
|
||||
const { push: navigate } = useNavigator();
|
||||
const setDocumentTitle = useDocumentTitle("Back from Mastodon...");
|
||||
const [siteImg, setSiteImg] = createSignal<{
|
||||
src: string;
|
||||
srcset?: string;
|
||||
blurhash: string;
|
||||
}>();
|
||||
const [siteTitle, setSiteTitle] = createSignal("the Mastodon server");
|
||||
const [siteTitle, setSiteTitle] = createSignal("Mastodon");
|
||||
|
||||
onMount(async () => {
|
||||
const onGoingOAuth2Process = $settings.get().onGoingOAuth2Process;
|
||||
|
@ -42,7 +41,6 @@ const MastodonOAuth2Callback: Component = () => {
|
|||
url: onGoingOAuth2Process,
|
||||
});
|
||||
const ins = await client.v2.instance.fetch();
|
||||
setDocumentTitle(`Back from ${ins.title}...`);
|
||||
setSiteTitle(ins.title);
|
||||
|
||||
const srcset = [];
|
||||
|
@ -93,42 +91,45 @@ const MastodonOAuth2Callback: Component = () => {
|
|||
});
|
||||
});
|
||||
return (
|
||||
<div class={cards.layoutCentered}>
|
||||
<div class={cards.card} aria-busy="true" aria-describedby={progressId}>
|
||||
<LinearProgress
|
||||
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
|
||||
id={progressId}
|
||||
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}
|
||||
<>
|
||||
<DocumentTitle>Back from {siteTitle()}</DocumentTitle>
|
||||
<div class={cards.layoutCentered}>
|
||||
<div class={cards.card} aria-busy="true" aria-describedby={progressId}>
|
||||
<LinearProgress
|
||||
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
|
||||
alt={`Banner image for ${siteTitle()}`}
|
||||
style={{ height: "235px", display: "block" }}
|
||||
id={progressId}
|
||||
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}>
|
||||
Contracting {siteTitle}...
|
||||
</Title>
|
||||
<p>
|
||||
If this page stays too long, you can close this page and sign in
|
||||
again.
|
||||
</p>
|
||||
<Title component="h6" id={titleId}>
|
||||
Contracting {siteTitle}...
|
||||
</Title>
|
||||
<p>
|
||||
If this page stays too long, you can close this page and sign in
|
||||
again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import {
|
|||
Component,
|
||||
Show,
|
||||
createEffect,
|
||||
createSelector,
|
||||
createSignal,
|
||||
createUniqueId,
|
||||
onMount,
|
||||
|
@ -10,15 +9,14 @@ import {
|
|||
import cards from "~material/cards.module.css";
|
||||
import TextField from "~material/TextField.js";
|
||||
import Button from "~material/Button.js";
|
||||
import { useDocumentTitle } from "../utils";
|
||||
import { Title } from "~material/typography";
|
||||
import { css } from "solid-styled";
|
||||
import { LinearProgress } from "@suid/material";
|
||||
import { createRestAPIClient } from "masto";
|
||||
import { getOrRegisterApp } from "./stores";
|
||||
import { useSearchParams } from "@solidjs/router";
|
||||
import { $settings } from "../settings/stores";
|
||||
import "./SignIn.css";
|
||||
import DocumentTitle from "~platform/DocumentTitle";
|
||||
|
||||
type ErrorParams = {
|
||||
error: string;
|
||||
|
@ -36,8 +34,6 @@ const SignIn: Component = () => {
|
|||
const [serverUrlError, setServerUrlError] = createSignal(false);
|
||||
const [targetSiteTitle, setTargetSiteTitle] = createSignal("");
|
||||
|
||||
useDocumentTitle("Sign In");
|
||||
|
||||
const serverUrl = () => {
|
||||
const url = rawServerUrl();
|
||||
if (url.length === 0 || /^%w:/.test(url)) {
|
||||
|
@ -115,55 +111,58 @@ const SignIn: Component = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<main class="SignIn">
|
||||
<Show when={params.error || params.errorDescription}>
|
||||
<div class={cards.card} style={{ "margin-bottom": "20px" }}>
|
||||
<p>Authorization is failed.</p>
|
||||
<p>{params.errorDescription}</p>
|
||||
<p>
|
||||
Please try again later. If the problem persists, you can ask for
|
||||
help from the server administrator.
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
<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>
|
||||
<>
|
||||
<DocumentTitle>Sign In</DocumentTitle>
|
||||
<main class="SignIn">
|
||||
<Show when={params.error || params.errorDescription}>
|
||||
<div class={cards.card} style={{ "margin-bottom": "20px" }}>
|
||||
<p>Authorization is failed.</p>
|
||||
<p>{params.errorDescription}</p>
|
||||
<p>
|
||||
Please try again later. If the problem persists, you can ask for
|
||||
help from the server administrator.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</Show>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -3,14 +3,12 @@ import {
|
|||
splitProps,
|
||||
Component,
|
||||
createSignal,
|
||||
createEffect,
|
||||
onMount,
|
||||
createRenderEffect,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { css } from "solid-styled";
|
||||
import { decode } from "blurhash";
|
||||
import { mergeClass } from "../utils";
|
||||
|
||||
type ImgProps = {
|
||||
blurhash?: string;
|
||||
|
@ -24,6 +22,7 @@ const Img: Component<ImgProps> = (props) => {
|
|||
"blurhash",
|
||||
"keepBlur",
|
||||
"class",
|
||||
"classList",
|
||||
"style",
|
||||
]);
|
||||
const [isImgLoaded, setIsImgLoaded] = createSignal(false);
|
||||
|
@ -61,21 +60,21 @@ const Img: Component<ImgProps> = (props) => {
|
|||
const onImgLoaded = () => {
|
||||
setIsImgLoaded(true);
|
||||
setImgSize({
|
||||
width: imgE.width,
|
||||
height: imgE.height,
|
||||
width: imgE!.width,
|
||||
height: imgE!.height,
|
||||
});
|
||||
};
|
||||
|
||||
const onMetadataLoaded = () => {
|
||||
setImgSize({
|
||||
width: imgE.width,
|
||||
height: imgE.height,
|
||||
width: imgE!.width,
|
||||
height: imgE!.height,
|
||||
});
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
setImgSize((x) => {
|
||||
const parent = imgE.parentElement;
|
||||
const parent = imgE!.parentElement;
|
||||
if (!parent) return x;
|
||||
return x
|
||||
? x
|
||||
|
@ -87,7 +86,14 @@ const Img: Component<ImgProps> = (props) => {
|
|||
});
|
||||
|
||||
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}>
|
||||
<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";
|
||||
import AppTopBar from "~material/AppTopBar";
|
||||
import type { Account } from "../accounts/stores";
|
||||
import DocumentTitle from "~platform/DocumentTitle";
|
||||
|
||||
const Profile: Component = () => {
|
||||
const { pop } = useNavigator();
|
||||
|
@ -216,355 +217,360 @@ const Profile: Component = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Scaffold
|
||||
topbar={
|
||||
<AppTopBar
|
||||
role="navigation"
|
||||
position="static"
|
||||
color={scrolledPastBanner() ? "primary" : "transparent"}
|
||||
elevation={scrolledPastBanner() ? undefined : 0}
|
||||
style={{
|
||||
color: scrolledPastBanner()
|
||||
? undefined
|
||||
: bannerSampledColors()?.text,
|
||||
}}
|
||||
>
|
||||
<IconButton color="inherit" onClick={[pop, 1]} aria-label="Close">
|
||||
<Close />
|
||||
</IconButton>
|
||||
<Title
|
||||
class="Profile__page-title"
|
||||
<>
|
||||
<DocumentTitle>{profile()?.displayName ?? "Someone"}</DocumentTitle>
|
||||
<Scaffold
|
||||
topbar={
|
||||
<AppTopBar
|
||||
role="navigation"
|
||||
position="static"
|
||||
color={scrolledPastBanner() ? "primary" : "transparent"}
|
||||
elevation={scrolledPastBanner() ? undefined : 0}
|
||||
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>
|
||||
</AppTopBar>
|
||||
}
|
||||
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>
|
||||
</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>
|
||||
}
|
||||
<IconButton color="inherit" onClick={[pop, 1]} aria-label="Close">
|
||||
<Close />
|
||||
</IconButton>
|
||||
<Title
|
||||
class="Profile__page-title"
|
||||
style={{
|
||||
visibility: scrolledPastBanner() ? undefined : "hidden",
|
||||
}}
|
||||
innerHTML={displayName()}
|
||||
></Title>
|
||||
|
||||
<IconButton
|
||||
id={menuButId}
|
||||
aria-controls={optMenuId}
|
||||
color="inherit"
|
||||
onClick={[setMenuOpen, true]}
|
||||
aria-label="Open Options for the Profile"
|
||||
>
|
||||
<MenuItem disabled>
|
||||
<ListItemIcon>
|
||||
<Edit />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Edit...</ListItemText>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
</AppTopBar>
|
||||
}
|
||||
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>
|
||||
</Show>
|
||||
<Divider />
|
||||
</Show>
|
||||
<MenuItem disabled>
|
||||
<ListItemIcon>
|
||||
<Group />
|
||||
</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"
|
||||
<Show when={session().account && profile()}>
|
||||
<Show
|
||||
when={isCurrentSessionProfile()}
|
||||
fallback={
|
||||
<MenuItem
|
||||
onClick={(event) => {
|
||||
openSubscribeMenu(
|
||||
event.currentTarget.getBoundingClientRect(),
|
||||
);
|
||||
const { left, right, top } =
|
||||
event.currentTarget.getBoundingClientRect();
|
||||
openSubscribeMenu({
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
e: 1,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{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`}
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
<ListItemIcon>
|
||||
<PlaylistAdd />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Subscribe...</ListItemText>
|
||||
</MenuItem>
|
||||
}
|
||||
>
|
||||
<MenuItem disabled>
|
||||
<ListItemIcon>
|
||||
<Edit />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Edit...</ListItemText>
|
||||
</MenuItem>
|
||||
</Show>
|
||||
<Divider />
|
||||
</Show>
|
||||
<TootList
|
||||
id={recentTootListId}
|
||||
threads={recentToots.list}
|
||||
onUnknownThread={recentToots.getPath}
|
||||
onChangeToot={recentToots.set}
|
||||
/>
|
||||
</TimeSourceProvider>
|
||||
</ItemSelectionProvider>
|
||||
|
||||
<Show when={!recentTootChunk()?.done}>
|
||||
<MenuItem disabled>
|
||||
<ListItemIcon>
|
||||
<Group />
|
||||
</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={{
|
||||
"text-align": "center",
|
||||
"padding-bottom": "var(--safe-area-inset-bottom)",
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
aria-label="Load More"
|
||||
aria-controls={recentTootListId}
|
||||
size="large"
|
||||
color="primary"
|
||||
onClick={[refetchRecentToots, "prev"]}
|
||||
disabled={isTootListLoading()}
|
||||
<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) => {
|
||||
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 />}>
|
||||
<CircularProgress sx={{ width: "24px", height: "24px" }} />
|
||||
</Show>
|
||||
</IconButton>
|
||||
<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>
|
||||
</Show>
|
||||
</div>
|
||||
</Scaffold>
|
||||
</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 />
|
||||
</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 { useNavigator } from "~platform/StackedRouter";
|
||||
import AppTopBar from "~material/AppTopBar";
|
||||
import DocumentTitle from "~platform/DocumentTitle";
|
||||
|
||||
const ChooseLang: Component = () => {
|
||||
const { pop } = useNavigator();
|
||||
|
@ -53,67 +54,70 @@ const ChooseLang: Component = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Scaffold
|
||||
topbar={
|
||||
<AppTopBar>
|
||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
<Title>{t("Choose Language")}</Title>
|
||||
</AppTopBar>
|
||||
}
|
||||
>
|
||||
<List
|
||||
sx={{
|
||||
paddingBottom: "var(--safe-area-inset-bottom, 0)",
|
||||
}}
|
||||
<>
|
||||
<DocumentTitle>{t("Choose Language")}</DocumentTitle>
|
||||
<Scaffold
|
||||
topbar={
|
||||
<AppTopBar>
|
||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
<Title>{t("Choose Language")}</Title>
|
||||
</AppTopBar>
|
||||
}
|
||||
>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
onCodeChange(code() ? undefined : matchedLangCode());
|
||||
<List
|
||||
sx={{
|
||||
paddingBottom: "var(--safe-area-inset-bottom, 0)",
|
||||
}}
|
||||
>
|
||||
<ListItemText>
|
||||
{t("lang.auto", {
|
||||
detected: t(`lang.${matchedLangCode()}`) ?? matchedLangCode(),
|
||||
})}
|
||||
</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Switch checked={typeof code() === "undefined"} />
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
|
||||
<For each={SUPPORTED_LANGS}>
|
||||
{(c) => (
|
||||
<ListItemButton
|
||||
disabled={typeof code() === "undefined"}
|
||||
onClick={[onCodeChange, c]}
|
||||
>
|
||||
<ListItemText>{t(`lang.${c}`)}</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Radio
|
||||
checked={
|
||||
code() === c ||
|
||||
(code() === undefined && matchedLangCode() == c)
|
||||
}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
)}
|
||||
</For>
|
||||
</List>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
onCodeChange(code() ? undefined : matchedLangCode());
|
||||
}}
|
||||
>
|
||||
<ListItemText>
|
||||
{t("lang.auto", {
|
||||
detected: t(`lang.${matchedLangCode()}`) ?? matchedLangCode(),
|
||||
})}
|
||||
</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Switch checked={typeof code() === "undefined"} />
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
|
||||
<For each={SUPPORTED_LANGS}>
|
||||
{(c) => (
|
||||
<ListItemButton
|
||||
disabled={typeof code() === "undefined"}
|
||||
onClick={[onCodeChange, c]}
|
||||
>
|
||||
<ListItemText>{t(`lang.${c}`)}</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Radio
|
||||
checked={
|
||||
code() === c ||
|
||||
(code() === undefined && matchedLangCode() == c)
|
||||
}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
)}
|
||||
</For>
|
||||
</List>
|
||||
|
||||
<List subheader={<ListSubheader>{t("Unsupported")}</ListSubheader>}>
|
||||
<For each={unsupportedLangCodes()}>
|
||||
{(code) => (
|
||||
<ListItem>
|
||||
<ListItemText>{iso639_1.getNativeName(code)}</ListItemText>
|
||||
</ListItem>
|
||||
)}
|
||||
</For>
|
||||
<List subheader={<ListSubheader>{t("Unsupported")}</ListSubheader>}>
|
||||
<For each={unsupportedLangCodes()}>
|
||||
{(code) => (
|
||||
<ListItem>
|
||||
<ListItemText>{iso639_1.getNativeName(code)}</ListItemText>
|
||||
</ListItem>
|
||||
)}
|
||||
</For>
|
||||
</List>
|
||||
</List>
|
||||
</List>
|
||||
</Scaffold>
|
||||
</Scaffold>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -19,9 +19,10 @@ import { useStore } from "@nanostores/solid";
|
|||
import { $settings } from "./stores";
|
||||
import { useNavigator } from "~platform/StackedRouter";
|
||||
import AppTopBar from "~material/AppTopBar";
|
||||
import DocumentTitle from "~platform/DocumentTitle";
|
||||
|
||||
const Motions: Component = () => {
|
||||
const {pop} = useNavigator();
|
||||
const { pop } = useNavigator();
|
||||
const [t] = createTranslator(
|
||||
(code) =>
|
||||
import(`./i18n/${code}.json`) as Promise<{
|
||||
|
@ -30,55 +31,58 @@ const Motions: Component = () => {
|
|||
);
|
||||
const settings = useStore($settings);
|
||||
return (
|
||||
<Scaffold
|
||||
topbar={
|
||||
<AppTopBar>
|
||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||
<>
|
||||
<DocumentTitle>{t("motions")}</DocumentTitle>
|
||||
<Scaffold
|
||||
topbar={
|
||||
<AppTopBar>
|
||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
<Title>{t("motions")}</Title>
|
||||
</AppTopBar>
|
||||
}
|
||||
>
|
||||
<List
|
||||
sx={{
|
||||
paddingBottom: "calc(var(--safe-area-inset-bottom, 0px) + 16px)",
|
||||
}}
|
||||
</AppTopBar>
|
||||
}
|
||||
>
|
||||
<li>
|
||||
<ul style={{ "padding-left": 0 }}>
|
||||
<ListSubheader>{t("motions.gifs")}</ListSubheader>
|
||||
<ListItemButton
|
||||
onClick={() =>
|
||||
$settings.setKey("autoPlayGIFs", !settings().autoPlayGIFs)
|
||||
}
|
||||
>
|
||||
<ListItemText>{t("motions.gifs.autoplay")}</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Switch checked={settings().autoPlayGIFs}></Switch>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider />
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul style={{ "padding-left": 0 }}>
|
||||
<ListSubheader>{t("motions.vids")}</ListSubheader>
|
||||
<ListItemButton
|
||||
onClick={() =>
|
||||
$settings.setKey("autoPlayVideos", !settings().autoPlayVideos)
|
||||
}
|
||||
>
|
||||
<ListItemText>{t("motions.vids.autoplay")}</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Switch checked={settings().autoPlayVideos}></Switch>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider />
|
||||
</ul>
|
||||
</li>
|
||||
</List>
|
||||
</Scaffold>
|
||||
<List
|
||||
sx={{
|
||||
paddingBottom: "calc(var(--safe-area-inset-bottom, 0px) + 16px)",
|
||||
}}
|
||||
>
|
||||
<li>
|
||||
<ul style={{ "padding-left": 0 }}>
|
||||
<ListSubheader>{t("motions.gifs")}</ListSubheader>
|
||||
<ListItemButton
|
||||
onClick={() =>
|
||||
$settings.setKey("autoPlayGIFs", !settings().autoPlayGIFs)
|
||||
}
|
||||
>
|
||||
<ListItemText>{t("motions.gifs.autoplay")}</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Switch checked={settings().autoPlayGIFs}></Switch>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider />
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul style={{ "padding-left": 0 }}>
|
||||
<ListSubheader>{t("motions.vids")}</ListSubheader>
|
||||
<ListItemButton
|
||||
onClick={() =>
|
||||
$settings.setKey("autoPlayVideos", !settings().autoPlayVideos)
|
||||
}
|
||||
>
|
||||
<ListItemText>{t("motions.vids.autoplay")}</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Switch checked={settings().autoPlayVideos}></Switch>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider />
|
||||
</ul>
|
||||
</li>
|
||||
</List>
|
||||
</Scaffold>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import { $settings } from "./stores";
|
|||
import { useStore } from "@nanostores/solid";
|
||||
import { useNavigator } from "~platform/StackedRouter";
|
||||
import AppTopBar from "~material/AppTopBar";
|
||||
import DocumentTitle from "~platform/DocumentTitle";
|
||||
|
||||
const ChooseRegion: Component = () => {
|
||||
const { pop } = useNavigator();
|
||||
|
@ -48,59 +49,62 @@ const ChooseRegion: Component = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Scaffold
|
||||
topbar={
|
||||
<AppTopBar>
|
||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
<Title>{t("Choose Region")}</Title>
|
||||
</AppTopBar>
|
||||
}
|
||||
>
|
||||
<List
|
||||
sx={{
|
||||
paddingBottom: "var(--safe-area-inset-bottom, 0)",
|
||||
}}
|
||||
<>
|
||||
<DocumentTitle>{t("Choose Region")}</DocumentTitle>
|
||||
<Scaffold
|
||||
topbar={
|
||||
<AppTopBar>
|
||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
<Title>{t("Choose Region")}</Title>
|
||||
</AppTopBar>
|
||||
}
|
||||
>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
onCodeChange(region() ? undefined : matchedRegionCode());
|
||||
<List
|
||||
sx={{
|
||||
paddingBottom: "var(--safe-area-inset-bottom, 0)",
|
||||
}}
|
||||
>
|
||||
<ListItemText>
|
||||
{t("region.auto", {
|
||||
detected:
|
||||
t(`region.${matchedRegionCode()}`) ?? matchedRegionCode(),
|
||||
})}
|
||||
</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Switch checked={typeof region() === "undefined"} />
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
onCodeChange(region() ? undefined : matchedRegionCode());
|
||||
}}
|
||||
>
|
||||
<ListItemText>
|
||||
{t("region.auto", {
|
||||
detected:
|
||||
t(`region.${matchedRegionCode()}`) ?? matchedRegionCode(),
|
||||
})}
|
||||
</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Switch checked={typeof region() === "undefined"} />
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
|
||||
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
|
||||
<For each={SUPPORTED_REGIONS}>
|
||||
{(code) => (
|
||||
<ListItemButton
|
||||
disabled={typeof region() === "undefined"}
|
||||
onClick={[onCodeChange, code]}
|
||||
>
|
||||
<ListItemText>{t(`region.${code}`)}</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Radio
|
||||
checked={
|
||||
region() === code ||
|
||||
(region() === undefined && matchedRegionCode() == code)
|
||||
}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
)}
|
||||
</For>
|
||||
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
|
||||
<For each={SUPPORTED_REGIONS}>
|
||||
{(code) => (
|
||||
<ListItemButton
|
||||
disabled={typeof region() === "undefined"}
|
||||
onClick={[onCodeChange, code]}
|
||||
>
|
||||
<ListItemText>{t(`region.${code}`)}</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Radio
|
||||
checked={
|
||||
region() === code ||
|
||||
(region() === undefined && matchedRegionCode() == code)
|
||||
}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
)}
|
||||
</For>
|
||||
</List>
|
||||
</List>
|
||||
</List>
|
||||
</Scaffold>
|
||||
</Scaffold>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ import { makeAcctText, useSessions } from "../masto/clients.js";
|
|||
import { useNavigator } from "~platform/StackedRouter.jsx";
|
||||
import AppTopBar from "~material/AppTopBar.jsx";
|
||||
import MastodonLogo from "./MastodonLogo.jsx";
|
||||
import DocumentTitle from "~platform/DocumentTitle.jsx";
|
||||
|
||||
type Inset = {
|
||||
top?: number;
|
||||
|
@ -197,225 +198,228 @@ const Settings: Component = () => {
|
|||
}
|
||||
`;
|
||||
return (
|
||||
<Scaffold
|
||||
topbar={
|
||||
<AppTopBar>
|
||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Title>{t("Settings")}</Title>
|
||||
</AppTopBar>
|
||||
}
|
||||
>
|
||||
<List class="setting-list" use:solid-styled>
|
||||
<li>
|
||||
<ul>
|
||||
<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 ? (
|
||||
<>
|
||||
<DocumentTitle>{t("Settings")}</DocumentTitle>
|
||||
<Scaffold
|
||||
topbar={
|
||||
<AppTopBar>
|
||||
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Title>{t("Settings")}</Title>
|
||||
</AppTopBar>
|
||||
}
|
||||
>
|
||||
<List class="setting-list" use:solid-styled>
|
||||
<li>
|
||||
<ListSubheader>Developer Tools</ListSubheader>
|
||||
<ListItem
|
||||
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
|
||||
<ul>
|
||||
<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={
|
||||
window.screen?.orientation
|
||||
? undefined
|
||||
: "Unsupported on This Platform"
|
||||
settings$().language === undefined
|
||||
? t("lang.auto", {
|
||||
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>
|
||||
</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>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</List>
|
||||
</Scaffold>
|
||||
{import.meta.env.DEV ? (
|
||||
<li>
|
||||
<ListSubheader>Developer Tools</ListSubheader>
|
||||
<ListItem
|
||||
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,
|
||||
useTransition,
|
||||
} from "solid-js";
|
||||
import { useDocumentTitle } from "../utils";
|
||||
import Scaffold from "~material/Scaffold";
|
||||
import {
|
||||
ListItemSecondaryAction,
|
||||
|
@ -30,6 +29,7 @@ import {
|
|||
import AppTopBar from "~material/AppTopBar";
|
||||
import { createTranslator } from "~platform/i18n";
|
||||
import { useWindowSize } from "@solid-primitives/resize-observer";
|
||||
import DocumentTitle from "~platform/DocumentTitle";
|
||||
|
||||
type StringRes = Record<
|
||||
"tabs.home" | "tabs.trending" | "tabs.public" | "set.prefetch-toots",
|
||||
|
@ -38,7 +38,6 @@ type StringRes = Record<
|
|||
|
||||
const Home: ParentComponent = (props) => {
|
||||
let panelList: HTMLDivElement;
|
||||
useDocumentTitle("Timelines");
|
||||
const [t] = createTranslator(
|
||||
(code) => import(`./i18n/${code}.json`) as Promise<{ default: StringRes }>,
|
||||
);
|
||||
|
@ -179,6 +178,7 @@ const Home: ParentComponent = (props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<DocumentTitle>Timelines</DocumentTitle>
|
||||
<Scaffold
|
||||
topbar={
|
||||
<AppTopBar>
|
||||
|
|
|
@ -14,7 +14,6 @@ import cards from "~material/cards.module.css";
|
|||
import { css } from "solid-styled";
|
||||
import { createTimeSource, TimeSourceProvider } from "~platform/timesrc";
|
||||
import TootComposer from "./TootComposer";
|
||||
import { useDocumentTitle } from "../utils";
|
||||
import { createTimelineControlsForArray } from "../masto/timelines";
|
||||
import TootList from "./TootList";
|
||||
import "./TootBottomSheet.css";
|
||||
|
@ -26,6 +25,7 @@ import ItemSelectionProvider, {
|
|||
import AppTopBar from "~material/AppTopBar";
|
||||
import { fetchStatus } from "../masto/statuses";
|
||||
import { type Account } from "../accounts/stores";
|
||||
import DocumentTitle from "~platform/DocumentTitle";
|
||||
|
||||
const TootBottomSheet: Component = (props) => {
|
||||
const params = useParams<{ acct: string; id: string }>();
|
||||
|
@ -65,11 +65,11 @@ const TootBottomSheet: Component = (props) => {
|
|||
() => tootContext()?.descendants,
|
||||
);
|
||||
|
||||
useDocumentTitle(() => {
|
||||
const documentTitle = () => {
|
||||
const t = toot()?.reblog ?? toot();
|
||||
const name = t?.account.displayName ?? "Someone";
|
||||
return `${name}'s toot`;
|
||||
});
|
||||
};
|
||||
|
||||
const tootDisplayName = () => {
|
||||
const t = toot()?.reblog ?? toot();
|
||||
|
@ -163,6 +163,7 @@ const TootBottomSheet: Component = (props) => {
|
|||
}
|
||||
class="TootBottomSheet"
|
||||
>
|
||||
<DocumentTitle>{documentTitle()}</DocumentTitle>
|
||||
<div class="Scrollable">
|
||||
<TimeSourceProvider value={time}>
|
||||
<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