Compare commits

..

No commits in common. "69f7f37a2ce917763a4d4bc10a86bd66fff1958f" and "d3a7602fb58c6b37c987769e1a313a3d248666bf" have entirely different histories.

15 changed files with 44 additions and 638 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -27,11 +27,9 @@
"wrangler": "^3.78.2" "wrangler": "^3.78.2"
}, },
"dependencies": { "dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
"@nanostores/persistent": "^0.10.2", "@nanostores/persistent": "^0.10.2",
"@nanostores/solid": "^0.4.2", "@nanostores/solid": "^0.4.2",
"@solid-primitives/event-listener": "^2.3.3", "@solid-primitives/event-listener": "^2.3.3",
"@solid-primitives/i18n": "^2.1.1",
"@solid-primitives/intersection-observer": "^2.1.6", "@solid-primitives/intersection-observer": "^2.1.6",
"@solid-primitives/resize-observer": "^2.0.26", "@solid-primitives/resize-observer": "^2.0.26",
"@solidjs/router": "^0.14.5", "@solidjs/router": "^0.14.5",
@ -42,7 +40,6 @@
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"fast-average-color": "^9.4.0", "fast-average-color": "^9.4.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"iso-639-1": "^3.1.3",
"masto": "^6.8.0", "masto": "^6.8.0",
"nanostores": "^0.11.3", "nanostores": "^0.11.3",
"solid-js": "^1.8.22", "solid-js": "^1.8.22",

View file

@ -6,7 +6,6 @@ import {
createSignal, createSignal,
ErrorBoundary, ErrorBoundary,
lazy, lazy,
onCleanup,
} from "solid-js"; } from "solid-js";
import { useRootTheme } from "./material/mui.js"; import { useRootTheme } from "./material/mui.js";
import { import {
@ -16,7 +15,6 @@ import {
} from "./masto/clients.js"; } from "./masto/clients.js";
import { $accounts } from "./accounts/stores.js"; import { $accounts } from "./accounts/stores.js";
import { useStore } from "@nanostores/solid"; import { useStore } from "@nanostores/solid";
import { DateFnScope, useLanguage } from "./platform/i18n.jsx";
const AccountSignIn = lazy(() => import("./accounts/SignIn.js")); const AccountSignIn = lazy(() => import("./accounts/SignIn.js"));
const AccountMastodonOAuth2Callback = lazy( const AccountMastodonOAuth2Callback = lazy(
@ -49,7 +47,6 @@ const App: Component = () => {
const theme = useRootTheme(); const theme = useRootTheme();
const accts = useStore($accounts); const accts = useStore($accounts);
const clientStore = createSignal<Session[]>([]); const clientStore = createSignal<Session[]>([]);
const lang = useLanguage();
createRenderEffect(() => { createRenderEffect(() => {
const [, setClients] = clientStore; const [, setClients] = clientStore;
@ -58,16 +55,6 @@ const App: Component = () => {
); );
}); });
createRenderEffect(() => {
const root = document.querySelector(":root")!;
root.setAttribute("lang", lang());
});
onCleanup(() => {
const root = document.querySelector(":root")!;
root.removeAttribute("lang");
});
const UnexpectedError = lazy(() => import("./UnexpectedError.js")); const UnexpectedError = lazy(() => import("./UnexpectedError.js"));
return ( return (
@ -78,11 +65,9 @@ const App: Component = () => {
}} }}
> >
<ThemeProvider theme={theme()}> <ThemeProvider theme={theme()}>
<DateFnScope>
<ClientProvider value={clientStore}> <ClientProvider value={clientStore}>
<Routing /> <Routing />
</ClientProvider> </ClientProvider>
</DateFnScope>
</ThemeProvider> </ThemeProvider>
</ErrorBoundary> </ErrorBoundary>
); );

View file

@ -12,16 +12,11 @@ export function useSignedInProfiles() {
}); });
return [ return [
() => { () => {
try {
const value = accessor(); const value = accessor();
if (value) { if (!value) {
return value;
}
} catch (reason) {
console.error("useSignedInProfiles: update acct info failed", reason);
}
return sessions().map((x) => ({ ...x, inf: x.account.inf })); return sessions().map((x) => ({ ...x, inf: x.account.inf }));
}
return value;
}, },
tools, tools,
] as const; ] as const;

View file

@ -1,188 +0,0 @@
import {
ParentComponent,
createContext,
createMemo,
createResource,
useContext,
} from "solid-js";
import { match } from "@formatjs/intl-localematcher";
import { Accessor, createEffect, createSignal } from "solid-js";
import { $settings } from "../settings/stores";
import { enGB } from "date-fns/locale/en-GB";
import { useStore } from "@nanostores/solid";
import type { Locale } from "date-fns";
import { resolveTemplate, translator, type Template } from "@solid-primitives/i18n";
async function synchronised(
name: string,
callback: () => Promise<void> | void,
): Promise<void> {
await navigator.locks.request(name, callback);
}
export const SUPPORTED_LANGS = ["en", "zh-Hans"] as const;
export const SUPPORTED_REGIONS = ["en_US", "en_GB", "zh_CN"] as const;
const DEFAULT_LANG = "en";
/**
* Decide the using language for the user.
* @returns the selected language tag
*/
export function autoMatchLangTag() {
return match(Array.from(navigator.languages), SUPPORTED_LANGS, DEFAULT_LANG);
}
const DateFnLocaleCx = /* __@PURE__ */createContext<Accessor<Locale>>(() => enGB);
const cachedDateFnLocale: Record<string, Locale> = {
enGB,
};
export function autoMatchRegion() {
const regions = navigator.languages
.map((x) => {
const parts = x.split("_");
if (parts.length > 1) {
return parts[1];
}
return undefined;
})
.filter((x): x is string => !!x);
for (const r of regions) {
for (const available of SUPPORTED_REGIONS) {
if (available.toLowerCase().endsWith(r.toLowerCase())) {
return available;
}
}
}
return "en_GB";
}
export function useRegion() {
const appSettings = useStore($settings);
return createMemo(() => {
const settings = appSettings();
if (typeof settings.region !== "undefined") {
return settings.region;
} else {
return autoMatchRegion();
}
});
}
async function importDateFnLocale(tag: string): Promise<Locale> {
switch (tag.toLowerCase()) {
case "en_us":
return (await import("date-fns/locale/en-US")).enUS;
case "en_gb":
return (await import("date-fns/locale/en-GB")).enGB;
case "zh_cn":
return (await import("date-fns/locale/zh-CN")).zhCN;
default:
throw new TypeError(`unsupported tag "${tag}"`);
}
}
/**
* Provides runtime values and fetch dependencies for date-fns locale
*/
export const DateFnScope: ParentComponent = (props) => {
const [dateFnLocale, setDateFnLocale] = createSignal(enGB);
const region = useRegion();
createEffect(() => {
const dateFnLocaleName = region();
if (cachedDateFnLocale[dateFnLocaleName]) {
setDateFnLocale(cachedDateFnLocale[dateFnLocaleName]);
} else {
synchronised("i18n-wrapper-load-date-fns-locale", async () => {
if (cachedDateFnLocale[dateFnLocaleName]) {
setDateFnLocale(cachedDateFnLocale[dateFnLocaleName]);
return;
}
const target = `date-fns/locale/${dateFnLocaleName}`;
try {
const mod = await importDateFnLocale(dateFnLocaleName);
cachedDateFnLocale[dateFnLocaleName] = mod;
setDateFnLocale(mod);
} catch (reason) {
console.error(
{
act: "load-date-fns-locale",
stat: "failed",
reason,
target,
},
"failed to load date-fns locale",
);
}
});
}
});
return (
<DateFnLocaleCx.Provider value={dateFnLocale}>
{props.children}
</DateFnLocaleCx.Provider>
);
};
/**
* Get the {@link Locale} object for date-fns.
*
* This function must be using in {@link DateFnScope}
*
* @returns Accessor for Locale
*/
export function useDateFnLocale(): Accessor<Locale> {
const cx = useContext(DateFnLocaleCx);
return cx;
}
export function useLanguage() {
const settings = useStore($settings);
return () => settings().language || autoMatchLangTag();
}
type ImportFn<T> = (name: string) => Promise<{default: T}>
type ImportedModule<F> = F extends ImportFn<infer T> ? T: never
type MergedImportedModule<T> =
T extends [] ? {} :
T extends [infer I] ? ImportedModule<I> :
T extends [infer I, ...infer J] ? ImportedModule<I> & MergedImportedModule<J> : never
export function createStringResource<
T extends ImportFn<Record<string, string | Template<any> | undefined>>[],
>(...importFns: T) {
const language = useLanguage();
const cache: Record<string, MergedImportedModule<T>> = {};
return createResource(
() => [language()] as const,
async ([nlang]) => {
if (cache[nlang]) {
return cache[nlang];
}
const results = await Promise.all(importFns.map(x => x(nlang).then(v => v.default)))
const merged: MergedImportedModule<T> = Object.assign({}, ...results)
cache[nlang] = merged;
return merged;
},
);
}
export function createTranslator<T extends ImportFn<Record<string, string | Template<any> | undefined>>[],>(...importFns: T) {
const res = createStringResource(...importFns)
return [translator(res[0], resolveTemplate), res] as const
}

View file

@ -1,116 +0,0 @@
import { createMemo, For, type Component, type JSX } from "solid-js";
import Scaffold from "../material/Scaffold";
import {
AppBar,
IconButton,
List,
ListItem,
ListItemButton,
ListItemSecondaryAction,
ListItemText,
ListSubheader,
Radio,
Switch,
Toolbar,
} from "@suid/material";
import { Close as CloseIcon } from "@suid/icons-material";
import iso639_1 from "iso-639-1";
import {
autoMatchLangTag,
createTranslator,
SUPPORTED_LANGS,
} from "../platform/i18n";
import { Title } from "../material/typography";
import type { Template } from "@solid-primitives/i18n";
type ChooseLangProps = {
code?: string;
onCodeChange: (ncode?: string) => void;
onClose?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
};
const ChooseLang: Component<ChooseLangProps> = (props) => {
const [t] = createTranslator(
() => import("./i18n/lang-names.json"),
(code) =>
import(`./i18n/${code}.json`) as Promise<{
default: Record<string, string | undefined> & {
["lang.auto"]: Template<{ detected: string }>;
};
}>,
);
const unsupportedLangCodes = createMemo(() => {
return iso639_1.getAllCodes().filter((x) => !["zh", "en"].includes(x));
});
const matchedLangCode = createMemo(() => autoMatchLangTag());
return (
<Scaffold
topbar={
<AppBar position="static">
<Toolbar
variant="dense"
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
>
<IconButton color="inherit" onClick={props.onClose} disableRipple>
<CloseIcon />
</IconButton>
<Title>{t("Choose Language")}</Title>
</Toolbar>
</AppBar>
}
>
<List
sx={{
paddingBottom: "var(--safe-area-inset-bottom, 0)",
}}
>
<ListItemButton
onClick={() => {
props.onCodeChange(props.code ? undefined : matchedLangCode());
}}
>
<ListItemText>
{t("lang.auto", {
detected: t(`lang.${matchedLangCode()}`) ?? matchedLangCode(),
})}
</ListItemText>
<ListItemSecondaryAction>
<Switch checked={typeof props.code === "undefined"} />
</ListItemSecondaryAction>
</ListItemButton>
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
<For each={SUPPORTED_LANGS}>
{(code) => (
<ListItemButton
disabled={typeof props.code === "undefined"}
onClick={[props.onCodeChange, code]}
>
<ListItemText>{t(`lang.${code}`)}</ListItemText>
<ListItemSecondaryAction>
<Radio
checked={props.code === code || (props.code === undefined && matchedLangCode() == code)}
/>
</ListItemSecondaryAction>
</ListItemButton>
)}
</For>
</List>
<List subheader={<ListSubheader>{t("Unsupported")}</ListSubheader>}>
<For each={unsupportedLangCodes()}>
{(code) => (
<ListItem>
<ListItemText>{iso639_1.getNativeName(code)}</ListItemText>
</ListItem>
)}
</For>
</List>
</List>
</Scaffold>
);
};
export default ChooseLang;

View file

@ -1,106 +0,0 @@
import { createMemo, For, type Component, type JSX } from "solid-js";
import Scaffold from "../material/Scaffold";
import {
AppBar,
IconButton,
List,
ListItemButton,
ListItemSecondaryAction,
ListItemText,
ListSubheader,
Radio,
Switch,
Toolbar,
} from "@suid/material";
import { Close as CloseIcon } from "@suid/icons-material";
import iso639_1 from "iso-639-1";
import {
autoMatchRegion,
createTranslator,
SUPPORTED_REGIONS,
} from "../platform/i18n";
import { Title } from "../material/typography";
import type { Template } from "@solid-primitives/i18n";
type ChooseRegionProps = {
code?: string;
onCodeChange: (ncode?: string) => void;
onClose?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
};
const ChooseRegion: Component<ChooseRegionProps> = (props) => {
const [t] = createTranslator(
() => import("./i18n/lang-names.json"),
(code) =>
import(`./i18n/${code}.json`) as Promise<{
default: Record<string, string | undefined> & {
["lang.auto"]: Template<{ detected: string }>;
};
}>,
);
const unsupportedLangCodes = createMemo(() => {
return iso639_1.getAllCodes().filter((x) => !["zh", "en"].includes(x));
});
const matchedRegionCode = createMemo(() => autoMatchRegion());
return (
<Scaffold
topbar={
<AppBar position="static">
<Toolbar
variant="dense"
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
>
<IconButton color="inherit" onClick={props.onClose} disableRipple>
<CloseIcon />
</IconButton>
<Title>{t("Choose Language")}</Title>
</Toolbar>
</AppBar>
}
>
<List
sx={{
paddingBottom: "var(--safe-area-inset-bottom, 0)",
}}
>
<ListItemButton
onClick={() => {
props.onCodeChange(props.code ? undefined : matchedRegionCode());
}}
>
<ListItemText>
{t("region.auto", {
detected: t(`region.${matchedRegionCode()}`) ?? matchedRegionCode(),
})}
</ListItemText>
<ListItemSecondaryAction>
<Switch checked={typeof props.code === "undefined"} />
</ListItemSecondaryAction>
</ListItemButton>
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
<For each={SUPPORTED_REGIONS}>
{(code) => (
<ListItemButton
disabled={typeof props.code === "undefined"}
onClick={[props.onCodeChange, code]}
>
<ListItemText>{t(`region.${code}`)}</ListItemText>
<ListItemSecondaryAction>
<Radio
checked={props.code === code || (props.code === undefined && matchedRegionCode() == code)}
/>
</ListItemSecondaryAction>
</ListItemButton>
)}
</For>
</List>
</List>
</Scaffold>
);
};
export default ChooseRegion;

View file

@ -1,4 +1,4 @@
import { createSignal, For, Show, type ParentComponent } from "solid-js"; import { For, Show, type ParentComponent } from "solid-js";
import Scaffold from "../material/Scaffold.js"; import Scaffold from "../material/Scaffold.js";
import { import {
AppBar, AppBar,
@ -7,63 +7,32 @@ import {
List, List,
ListItem, ListItem,
ListItemButton, ListItemButton,
ListItemIcon,
ListItemSecondaryAction, ListItemSecondaryAction,
ListItemText, ListItemText,
ListSubheader, ListSubheader,
NativeSelect,
Switch, Switch,
Toolbar, Toolbar,
} from "@suid/material"; } from "@suid/material";
import { import {
Close as CloseIcon, Close as CloseIcon,
Logout,
Public as PublicIcon,
Refresh as RefreshIcon, Refresh as RefreshIcon,
Translate as TranslateIcon,
} from "@suid/icons-material"; } from "@suid/icons-material";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
import { Title } from "../material/typography.jsx"; import { Title } from "../material/typography.jsx";
import { css } from "solid-styled"; import { css } from "solid-styled";
import { useSignedInProfiles } from "../masto/acct.js"; import { useSignedInProfiles } from "../masto/acct.js";
import { signOut, type Account } from "../accounts/stores.js"; import { signOut, type Account } from "../accounts/stores.js";
import { format } from "date-fns"; import { intlFormat } from "date-fns";
import { useStore } from "@nanostores/solid"; import { useStore } from "@nanostores/solid";
import { $settings } from "./stores.js"; import { $settings } from "./stores.js";
import { useRegisterSW } from "virtual:pwa-register/solid"; import { useRegisterSW } from "virtual:pwa-register/solid";
import {
autoMatchLangTag,
autoMatchRegion,
createTranslator,
SUPPORTED_LANGS,
SUPPORTED_REGIONS,
useDateFnLocale,
} from "../platform/i18n.jsx";
import { type Template } from "@solid-primitives/i18n";
import BottomSheet from "../material/BottomSheet.jsx";
import ChooseLang from "./ChooseLang.jsx";
import ChooseRegion from "./ChooseRegion.jsx";
type Strings = {
["lang.auto"]: Template<{ detected: string }>;
} & Record<string, string | undefined>;
const Settings: ParentComponent = () => { const Settings: ParentComponent = () => {
const [t] = createTranslator(
(code) =>
import(`./i18n/${code}.json`) as Promise<{
default: Strings;
}>,
() => import(`./i18n/lang-names.json`),
);
const navigate = useNavigate(); const navigate = useNavigate();
const settings$ = useStore($settings); const settings$ = useStore($settings);
const { const {
needRefresh: [needRefresh], needRefresh: [needRefresh],
} = useRegisterSW(); } = useRegisterSW();
const dateFnLocale = useDateFnLocale();
const [langPickerOpen, setLangPickerOpen] = createSignal(false);
const [regionPickerOpen, setRegionPickerOpen] = createSignal(false);
const [profiles] = useSignedInProfiles(); const [profiles] = useSignedInProfiles();
@ -91,7 +60,7 @@ const Settings: ParentComponent = () => {
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple> <IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
<Title>{t("Settings")}</Title> <Title>Settings</Title>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
} }
@ -99,35 +68,32 @@ const Settings: ParentComponent = () => {
<List class="setting-list" use:solid-styled> <List class="setting-list" use:solid-styled>
<li> <li>
<ul> <ul>
<ListSubheader>{t("Accounts")}</ListSubheader> <ListSubheader>Accounts</ListSubheader>
<ListItemButton disabled> <ListItem>
<ListItemText>{t("All Notifications")}</ListItemText> <ListItemText>All Notifications</ListItemText>
<ListItemSecondaryAction> <ListItemSecondaryAction>
<Switch value={false} disabled /> <Switch value={false} />
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItemButton> </ListItem>
<Divider /> <Divider />
<ListItemButton disabled> <ListItem>
<ListItemText>{t("Sign in...")}</ListItemText> <ListItemText>Sign in...</ListItemText>
</ListItemButton> </ListItem>
<Divider /> <Divider />
</ul> </ul>
<For each={profiles()}> <For each={profiles()}>
{({ account: acct, inf }) => ( {({ account: acct, inf }) => (
<ul data-site={acct.site} data-username={inf?.username}> <ul data-site={acct.site} data-username={inf?.username}>
<ListSubheader>{`@${inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader> <ListSubheader>{`@${inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader>
<ListItemButton disabled> <ListItem>
<ListItemText>{t("Notifications")}</ListItemText> <ListItemText>Notifications</ListItemText>
<ListItemSecondaryAction> <ListItemSecondaryAction>
<Switch value={false} disabled /> <Switch value={false} />
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItemButton> </ListItem>
<Divider /> <Divider />
<ListItemButton onClick={[doSignOut, acct]}> <ListItemButton onClick={[doSignOut, acct]}>
<ListItemIcon> <ListItemText>Sign out</ListItemText>
<Logout />
</ListItemIcon>
<ListItemText>{t("Sign out")}</ListItemText>
</ListItemButton> </ListItemButton>
<Divider /> <Divider />
</ul> </ul>
@ -135,8 +101,12 @@ const Settings: ParentComponent = () => {
</For> </For>
</li> </li>
<li> <li>
<ListSubheader>{t("Reading")}</ListSubheader> <ListSubheader>Reading</ListSubheader>
<ListItemButton <ListItem>
<ListItemText secondary="Regular">Fonts</ListItemText>
</ListItem>
<Divider />
<ListItem
onClick={(e) => onClick={(e) =>
$settings.setKey( $settings.setKey(
"prefetchTootsDisabled", "prefetchTootsDisabled",
@ -144,93 +114,34 @@ const Settings: ParentComponent = () => {
) )
} }
> >
<ListItemText secondary={t("Prefetch Toots.2nd")}> <ListItemText secondary="Tutu will download toots before you scroll to the position.">
{t("Prefetch Toots")} Prefetch Toots
</ListItemText> </ListItemText>
<ListItemSecondaryAction> <ListItemSecondaryAction>
<Switch checked={!settings$().prefetchTootsDisabled} /> <Switch checked={!settings$().prefetchTootsDisabled} />
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItemButton> </ListItem>
<Divider /> <Divider />
</li> </li>
<li> <li>
<ListSubheader>{t("This Application")}</ListSubheader> <ListSubheader>This Application</ListSubheader>
<ListItemButton onClick={[setLangPickerOpen, true]}>
<ListItemIcon>
<TranslateIcon />
</ListItemIcon>
<ListItemText
secondary={
settings$().language === undefined
? t("lang.auto", {
detected:
t("lang." + autoMatchLangTag()) ?? autoMatchLangTag(),
})
: t("lang." + settings$().language)
}
>
{t("Language")}
</ListItemText>
</ListItemButton>
<BottomSheet open={langPickerOpen()}>
<ChooseLang
code={settings$().language}
onCodeChange={(nval) => $settings.setKey("language", nval)}
onClose={[setLangPickerOpen, false]}
/>
</BottomSheet>
<Divider />
<ListItemButton onClick={[setRegionPickerOpen, true]}>
<ListItemIcon>
<PublicIcon />
</ListItemIcon>
<ListItemText
secondary={
settings$().region === undefined
? t("region.auto", {
detected:
t("region." + autoMatchRegion()) ?? autoMatchRegion(),
})
: t("region." + settings$().region)
}
>
{t("Region")}
</ListItemText>
</ListItemButton>
<BottomSheet open={regionPickerOpen()}>
<ChooseRegion
code={settings$().region}
onCodeChange={(nval) => $settings.setKey("region", nval)}
onClose={[setRegionPickerOpen, false]}
/>
</BottomSheet>
<Divider />
<ListItem> <ListItem>
<ListItemText secondary={t("About Tutu.2nd")}> <ListItemText secondary="Comformtable tooting experience.">
{t("About Tutu")} About Tutu
</ListItemText> </ListItemText>
</ListItem> </ListItem>
<Divider /> <Divider />
<ListItem> <ListItem>
<ListItemText <ListItemText
secondary={t("version", { secondary={`Using v${import.meta.env.PACKAGE_VERSION} (built on ${intlFormat(import.meta.env.BUILT_AT)}, ${import.meta.env.MODE})`}
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")} {needRefresh()
? "An update is ready, restart the Tutu to apply"
: "No updates"}
</ListItemText> </ListItemText>
<Show when={needRefresh()}> <Show when={needRefresh()}>
<ListItemSecondaryAction> <ListItemSecondaryAction>
<IconButton <IconButton aria-label="Restart Now" onClick={() => window.location.reload()}>
aria-label="Restart Now"
onClick={() => window.location.reload()}
>
<RefreshIcon /> <RefreshIcon />
</IconButton> </IconButton>
</ListItemSecondaryAction> </ListItemSecondaryAction>

View file

@ -1,30 +0,0 @@
{
"Settings": "Settings",
"Accounts": "Accounts",
"All Notifications": "All Notifications",
"Sign in...": "Sign in...",
"Reading": "Reading",
"Prefetch Toots": "Prefetch Toots",
"Prefetch Toots.2nd": "Tutu will download the toots before you need.",
"This Application": "This Application",
"About Tutu": "About Tutu",
"About Tutu.2nd": "Comfortable tooting experience.",
"updates.ready": "An update is ready, restart the Tutu to apply",
"updates.no": "No updates",
"version": "Using v{{packageVersion}} (built on {{builtAt}}, {{buildMode}})",
"Language": "Language",
"Region": "Region",
"lang.auto": "(Auto) {{detected}}",
"region.auto": "(Auto) {{detected}}",
"region.en_GB": "Great Britan (English)",
"region.en_US": "United States (English)",
"region.zh_CN": "China Mainland (Chinese)",
"datefmt": "yyyy/MM/dd",
"Sign out": "Sign out",
"Notifications": "Notifications",
"Choose Language": "Choose Language",
"Supported": "Supported",
"Unsupported": "Unsupported",
"Choose Region": "Choose Region"
}

View file

@ -1,4 +0,0 @@
{
"lang.zh-Hans": "中文(简体)",
"lang.en": "English"
}

View file

@ -1,30 +0,0 @@
{
"Settings": "设置",
"Accounts": "所有账户",
"All Notifications": "所有通知",
"Sign in...": "登录新账户...",
"Reading": "阅读",
"Prefetch Toots": "提前下载嘟文",
"Prefetch Toots.2nd": "图图会在你可能需要的时候提前下载嘟文。",
"This Application": "本应用",
"About Tutu": "关于图图",
"About Tutu.2nd": "舒服地刷嘟。",
"updates.ready": "更新已准备好,下次开启会启动新版本",
"updates.no": "已是最新版本",
"version": "正在使用 v{{packageVersion}} ({{builtAt}}构建, {{buildMode}})",
"Language": "语言",
"Region": "区域",
"lang.auto": "(自动){{detected}}",
"region.auto": "(自动){{detected}}",
"region.en_GB": "英国和苏格兰(英语)",
"region.en_US": "美国(英语)",
"region.zh_CN": "中国大陆(中文)",
"datefmt": "yyyy年MM月dd日",
"Sign out": "登出此账户",
"Notifications": "通知",
"Choose Language": "选择语言",
"Supported": "已支持",
"Unsupported": "尚未支持",
"Choose Region": "选择区域"
}

View file

@ -3,8 +3,6 @@ import { persistentMap } from "@nanostores/persistent";
type Settings = { type Settings = {
onGoingOAuth2Process?: string; onGoingOAuth2Process?: string;
prefetchTootsDisabled?: boolean; prefetchTootsDisabled?: boolean;
language?: string;
region?: string;
}; };
export const $settings = persistentMap<Settings>( export const $settings = persistentMap<Settings>(

View file

@ -10,8 +10,7 @@ import {
children, children,
Suspense, Suspense,
Match, Match,
Switch as JsSwitch, Switch as JsSwitch
ErrorBoundary
} from "solid-js"; } from "solid-js";
import { useDocumentTitle } from "../utils"; import { useDocumentTitle } from "../utils";
import { type mastodon } from "masto"; import { type mastodon } from "masto";
@ -126,9 +125,7 @@ const TimelinePanel: Component<{
}; };
return ( return (
<ErrorBoundary fallback={(err, reset) => { <>
return <p>Oops: {String(err)}</p>
}}>
<PullDownToRefresh <PullDownToRefresh
linkedElement={scrollLinked()} linkedElement={scrollLinked()}
loading={snapshot.loading} loading={snapshot.loading}
@ -205,7 +202,7 @@ const TimelinePanel: Component<{
</Match> </Match>
</JsSwitch> </JsSwitch>
</div> </div>
</ErrorBoundary> </>
); );
}; };

View file

@ -36,7 +36,6 @@ import Button from "../material/Button.js";
import MediaAttachmentGrid from "./MediaAttachmentGrid.js"; import MediaAttachmentGrid from "./MediaAttachmentGrid.js";
import { FastAverageColor } from "fast-average-color"; import { FastAverageColor } from "fast-average-color";
import Color from "colorjs.io"; import Color from "colorjs.io";
import { useDateFnLocale } from "../platform/i18n";
type TootContentViewProps = { type TootContentViewProps = {
source?: string; source?: string;
@ -171,7 +170,6 @@ function TootActionGroup<T extends mastodon.v1.Status>(
function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) { function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) {
const toot = () => props.status; const toot = () => props.status;
const dateFnLocale = useDateFnLocale()
return ( return (
<div class={tootStyle.tootAuthorGrp}> <div class={tootStyle.tootAuthorGrp}>
@ -189,7 +187,7 @@ function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) {
}} }}
/> />
<time datetime={toot().createdAt}> <time datetime={toot().createdAt}>
{formatRelative(toot().createdAt, props.now, {locale: dateFnLocale()})} {formatRelative(toot().createdAt, props.now)}
</time> </time>
<span> <span>
@{toot().account.username}@{new URL(toot().account.url).hostname} @{toot().account.username}@{new URL(toot().account.url).hostname}

View file

@ -11,6 +11,5 @@
"types": ["vite/client", "vite-plugin-pwa/solid"], "types": ["vite/client", "vite-plugin-pwa/solid"],
"noEmit": true, "noEmit": true,
"isolatedModules": true, "isolatedModules": true,
"resolveJsonModule": true,
} }
} }