Compare commits
No commits in common. "69f7f37a2ce917763a4d4bc10a86bd66fff1958f" and "d3a7602fb58c6b37c987769e1a313a3d248666bf" have entirely different histories.
69f7f37a2c
...
d3a7602fb5
15 changed files with 44 additions and 638 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -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",
|
||||||
|
|
15
src/App.tsx
15
src/App.tsx
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"lang.zh-Hans": "中文(简体)",
|
|
||||||
"lang.en": "English"
|
|
||||||
}
|
|
|
@ -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": "选择区域"
|
|
||||||
}
|
|
|
@ -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>(
|
||||||
|
|
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue