added settings

This commit is contained in:
thislight 2024-07-22 21:57:04 +08:00
parent 5ec00d4999
commit 5833e5a76b
15 changed files with 359 additions and 91 deletions

View file

@ -20,6 +20,7 @@
"prettier": "^3.3.2",
"typescript": "^5.5.2",
"vite": "^5.3.2",
"vite-plugin-package-version": "^1.1.0",
"vite-plugin-pwa": "^0.20.0",
"vite-plugin-solid": "^2.10.2",
"vite-plugin-solid-styled": "^0.11.1",

View file

@ -69,6 +69,9 @@ importers:
vite:
specifier: ^5.3.2
version: 5.3.2(@types/node@20.14.10)(lightningcss@1.25.1)(terser@5.31.2)
vite-plugin-package-version:
specifier: ^1.1.0
version: 1.1.0(vite@5.3.2(@types/node@20.14.10)(lightningcss@1.25.1)(terser@5.31.2))
vite-plugin-pwa:
specifier: ^0.20.0
version: 0.20.0(vite@5.3.2(@types/node@20.14.10)(lightningcss@1.25.1)(terser@5.31.2))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0)
@ -2489,6 +2492,11 @@ packages:
validate-html-nesting@1.2.2:
resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==}
vite-plugin-package-version@1.1.0:
resolution: {integrity: sha512-TPoFZXNanzcaKCIrC3e2L/TVRkkRLB6l4RPN/S7KbG7rWfyLcCEGsnXvxn6qR7fyZwXalnnSN/I9d6pSFjHpEA==}
peerDependencies:
vite: '>=2.0.0-beta.69'
vite-plugin-pwa@0.20.0:
resolution: {integrity: sha512-/kDZyqF8KqoXRpMUQtR5Atri/7BWayW8Gp7Kz/4bfstsV6zSFTxjREbXZYL7zSuRL40HGA+o2hvUAFRmC+bL7g==}
engines: {node: '>=16.0.0'}
@ -5280,6 +5288,10 @@ snapshots:
validate-html-nesting@1.2.2: {}
vite-plugin-package-version@1.1.0(vite@5.3.2(@types/node@20.14.10)(lightningcss@1.25.1)(terser@5.31.2)):
dependencies:
vite: 5.3.2(@types/node@20.14.10)(lightningcss@1.25.1)(terser@5.31.2)
vite-plugin-pwa@0.20.0(vite@5.3.2(@types/node@20.14.10)(lightningcss@1.25.1)(terser@5.31.2))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0):
dependencies:
debug: 4.3.5

View file

@ -1,14 +1,4 @@
html,
body {
overflow: hidden;
height: 100vh;
height: 100dvh;
}
#root {
overflow: hidden hidden;
height: 100vh;
height: 100dvh;
:root {
background-color: var(--tutu-color-surface, transparent);
}

View file

@ -22,11 +22,15 @@ const AccountMastodonOAuth2Callback = lazy(
() => import("./accounts/MastodonOAuth2Callback.js"),
);
const TimelineHome = lazy(() => import("./timelines/Home.js"));
const Settings = lazy(() => import("./settings/Settings.js"));
const Routing: Component = () => {
return (
<Router>
<Route path="/" component={TimelineHome}></Route>
<Route path="/" component={TimelineHome}>
<Route path=""></Route>
<Route path="/settings" component={Settings}></Route>
</Route>
<Route path={"/accounts"}>
<Route path={"/sign-in"} component={AccountSignIn} />
<Route

View file

@ -1,6 +1,11 @@
import { persistentAtom } from "@nanostores/persistent";
import { createOAuthAPIClient, createRestAPIClient } from "masto";
import {
createOAuthAPIClient,
createRestAPIClient,
type mastodon,
} from "masto";
import { action } from "nanostores";
import { createMastoClientFor } from "../masto/clients";
export type Account = {
site: string;
@ -9,6 +14,8 @@ export type Account = {
tokenType: string;
scope: string;
createdAt: number;
inf?: mastodon.v1.AccountCredentials;
};
export const $accounts = persistentAtom<Account[]>("accounts", [], {
@ -75,6 +82,23 @@ export const acceptAccountViaAuthCode = action(
},
);
export const updateAcctInf = action(
$accounts,
"updateAcctInf",
async ($store, idx: number) => {
const o = $store.get();
const client = createMastoClientFor(o[idx]);
const inf = await client.v1.accounts.verifyCredentials();
o[idx].inf = inf;
$store.set(o);
return inf;
},
);
export const signOut = action($accounts, "signOut", ($store, predicate: (acct: Account) => boolean) => {
$store.set($store.get().filter(a => !predicate(a)));
});
export type RegisteredApp = {
site: string;
clientId: string;

View file

@ -1,10 +1,35 @@
import { Accessor, createResource } from "solid-js";
import type { mastodon } from "masto";
import { useSessions } from "./clients";
import { updateAcctInf } from "../accounts/stores";
export function useAcctProfile(client: Accessor<mastodon.rest.Client>) {
return createResource(client, (client) => {
return client.v1.accounts.verifyCredentials()
}, {
name: "MastodonAccountProfile"
})
return createResource(
client,
(client) => {
return client.v1.accounts.verifyCredentials();
},
{
name: "MastodonAccountProfile",
},
);
}
export function useSignedInProfiles() {
const sessions = useSessions();
const [accessor, tools] = createResource(sessions, async (all) => {
return Promise.all(
all.map(async (x, i) => ({ ...x, inf: await updateAcctInf(i) })),
);
});
return [
() => {
if (accessor.loading) {
accessor();
return sessions().map((x) => ({ ...x, inf: x.account.inf }));
}
return accessor();
},
tools,
] as const;
}

View file

@ -69,7 +69,7 @@ export function useSessions() {
return sessions;
}
export function useSessionsRw() {
function useSessionsRw() {
const store = useContext(Context);
if (!store) {
throw new TypeError("sessions are not provided");

View file

@ -0,0 +1,32 @@
.bottomSheet {
composes: surface from 'material.module.css';
border: none;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 0;
width: 100%;
max-width: 560px;
border-radius: 2px;
overscroll-behavior: contain;
box-shadow: var(--tutu-shadow-e16);
:global(.MuiToolbar-root) > :global(.MuiButtonBase-root):first-child {
color: white;
margin-left: -0.5em;
margin-right: 24px;
}
@media (max-width: 560px) {
& {
left: 0;
top: 0;
transform: none;
bottom: 0;
height: 100vh;
height: 100dvh;
}
}
}

View file

@ -0,0 +1,26 @@
import { createEffect, type ParentComponent } from "solid-js";
import styles from './BottomSheet.module.css'
export type BottomSheetProps = {
open?: boolean;
};
const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
let element: HTMLDialogElement;
createEffect(() => {
if (props.open) {
if (!element.open) {
element.showModal();
}
} else {
if (element.open) {
element.close();
}
}
});
return <dialog class={styles.bottomSheet} ref={element!}>{props.children}</dialog>;
};
export default BottomSheet;

View file

@ -22,9 +22,6 @@ const Scaffold: ParentComponent<ScaffoldProps> = (props) => {
css`
.scaffold-content {
--scaffold-topbar-height: ${(topbarSize.height?.toString() ?? 0) + "px"};
height: 100%;
width: 100%;
}
.topbar {

10
src/overrides.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly BUILT_AT: string;
readonly PACKAGE_VERSION: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

137
src/settings/Settings.tsx Normal file
View file

@ -0,0 +1,137 @@
import { createResource, For, type ParentComponent } from "solid-js";
import Scaffold from "../material/Scaffold.js";
import {
AppBar,
Divider,
IconButton,
List,
ListItem,
ListItemButton,
ListItemSecondaryAction,
ListItemText,
ListSubheader,
NativeSelect,
Select,
Switch,
Toolbar,
} from "@suid/material";
import { Close as CloseIcon } from "@suid/icons-material";
import { useNavigate } from "@solidjs/router";
import { Title } from "../material/typography.jsx";
import { useSessions } from "../masto/clients.js";
import { css } from "solid-styled";
import { useSignedInProfiles } from "../masto/acct.js";
import { signOut, type Account } from "../accounts/stores.js";
import { intlFormat } from "date-fns";
const Settings: ParentComponent = () => {
const navigate = useNavigate();
const [profiles] = useSignedInProfiles();
const doSignOut = (acct: Account) => {
signOut((a) => a.site === acct.site && a.accessToken === acct.accessToken);
};
css`
ul {
padding: 0;
}
.setting-list {
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
}
`;
return (
<Scaffold
topbar={
<AppBar position="static">
<Toolbar variant="dense">
<IconButton onClick={[navigate, -1]}>
<CloseIcon />
</IconButton>
<Title>Settings</Title>
</Toolbar>
</AppBar>
}
>
<List class="setting-list" use:solid-styled>
<li>
<ul>
<ListSubheader>Accounts</ListSubheader>
<ListItem>
<ListItemText>All Notifications</ListItemText>
<ListItemSecondaryAction>
<Switch />
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemText>Sign in...</ListItemText>
</ListItem>
</ul>
<For each={profiles()}>
{({ account: acct, inf }) => (
<ul data-site={acct.site} data-username={inf?.username}>
<ListSubheader>{`@${inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader>
<ListItem>
<ListItemText>Notifications</ListItemText>
<ListItemSecondaryAction>
<Switch />
</ListItemSecondaryAction>
</ListItem>
<ListItemButton onClick={[doSignOut, acct]}>
<ListItemText>Sign out</ListItemText>
</ListItemButton>
</ul>
)}
</For>
</li>
<li>
<ListSubheader>Reading</ListSubheader>
<ListItem>
<ListItemText secondary="Regular">Fonts</ListItemText>
</ListItem>
<ListItem>
<ListItemText secondary="Tutu will download toots before you scroll to the position.">
Prefetch Toots
</ListItemText>
<ListItemSecondaryAction>
<Switch />
</ListItemSecondaryAction>
</ListItem>
</li>
<li>
<ListSubheader>Controls</ListSubheader>
<ListItem>
<ListItemText>Optimized UI</ListItemText>
<ListItemSecondaryAction>
<NativeSelect value="auto">
<option value="auto">Tutu Decides (Mouse)</option>
<option value="mouse">Mouse</option>
<option value="touch">Touch</option>
<option value="controlpad">Control Pad</option>
</NativeSelect>
</ListItemSecondaryAction>
</ListItem>
</li>
<li>
<ListSubheader>This Application</ListSubheader>
<ListItem>
<ListItemText secondary="Comformtable tooting experience.">
About Tutu
</ListItemText>
</ListItem>
<ListItem>
<ListItemText
secondary={`Using v${import.meta.env.PACKAGE_VERSION} (built on ${intlFormat(import.meta.env.BUILT_AT)}, ${import.meta.env.MODE})`}
>
No updates
</ListItemText>
</ListItem>
</li>
</List>
</Scaffold>
);
};
export default Settings;

View file

@ -6,9 +6,11 @@ import {
Show,
untrack,
onMount,
type ParentComponent,
children,
} from "solid-js";
import { useDocumentTitle } from "../utils";
import { useSessions } from "../masto/clients";
import { useSessions } from "../masto/clients";
import { type mastodon } from "masto";
import Scaffold from "../material/Scaffold";
import {
@ -32,6 +34,7 @@ import Tab from "../material/Tab";
import { Create as CreateTootIcon } from "@suid/icons-material";
import { useTimeline } from "../masto/timelines";
import { makeEventListener } from "@solid-primitives/event-listener";
import BottomSheet from "../material/BottomSheet";
const TimelinePanel: Component<{
client: mastodon.rest.Client;
@ -145,7 +148,7 @@ const TimelinePanel: Component<{
);
};
const Home: Component = () => {
const Home: ParentComponent = (props) => {
let panelList: HTMLDivElement;
useDocumentTitle("Timelines");
const now = createTimeSource();
@ -162,6 +165,8 @@ const Home: Component = () => {
number,
]);
const child = children(() => props.children)
let scrollEventLockReleased = true;
const recalculateTabIndicator = () => {
@ -221,7 +226,7 @@ const Home: Component = () => {
const onTabClick = (idx: number) => {
const items = panelList.querySelectorAll(".tab-panel");
if (items.length > idx) {
items.item(idx).scrollIntoView({ behavior: "smooth" });
items.item(idx).scrollIntoView({ block: "nearest", behavior: "smooth" });
}
};
@ -235,10 +240,6 @@ const Home: Component = () => {
max-height: calc(100dvh - var(--scaffold-topbar-height, 0px));
scroll-snap-align: center;
&:not(.active) {
overflow: hidden;
}
@media (max-width: 600px) {
padding: 0;
}
@ -261,71 +262,74 @@ const Home: Component = () => {
`;
return (
<Scaffold
topbar={
<AppBar position="static">
<Toolbar variant="dense" class="responsive">
<Tabs onFocusChanged={setCurrentFocusOn} offset={panelOffset()}>
<Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
Home
</Tab>
<Tab focus={isTabFocus(1)} onClick={[onTabClick, 1]}>
Trending
</Tab>
<Tab focus={isTabFocus(2)} onClick={[onTabClick, 2]}>
Public
</Tab>
</Tabs>
<ProfileMenuButton profile={profile()}>
<MenuItem onClick={(e) => setPrefetching((x) => !x)}>
<ListItemText>Prefetch Toots</ListItemText>
<ListItemSecondaryAction>
<Switch checked={prefetching()}></Switch>
</ListItemSecondaryAction>
</MenuItem>
</ProfileMenuButton>
</Toolbar>
</AppBar>
}
fab={
<Fab color="secondary">
<CreateTootIcon />
</Fab>
}
>
<TimeSourceProvider value={now}>
<div class="panel-list" ref={panelList!}>
<div class="tab-panel">
<div>
<TimelinePanel
client={client()}
name="home"
prefetch={prefetching()}
/>
<>
<Scaffold
topbar={
<AppBar position="static">
<Toolbar variant="dense" class="responsive">
<Tabs onFocusChanged={setCurrentFocusOn} offset={panelOffset()}>
<Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
Home
</Tab>
<Tab focus={isTabFocus(1)} onClick={[onTabClick, 1]}>
Trending
</Tab>
<Tab focus={isTabFocus(2)} onClick={[onTabClick, 2]}>
Public
</Tab>
</Tabs>
<ProfileMenuButton profile={profile()}>
<MenuItem onClick={(e) => setPrefetching((x) => !x)}>
<ListItemText>Prefetch Toots</ListItemText>
<ListItemSecondaryAction>
<Switch checked={prefetching()}></Switch>
</ListItemSecondaryAction>
</MenuItem>
</ProfileMenuButton>
</Toolbar>
</AppBar>
}
fab={
<Fab color="secondary">
<CreateTootIcon />
</Fab>
}
>
<TimeSourceProvider value={now}>
<div class="panel-list" ref={panelList!}>
<div class="tab-panel">
<div>
<TimelinePanel
client={client()}
name="home"
prefetch={prefetching()}
/>
</div>
</div>
</div>
<div class="tab-panel">
<div>
<TimelinePanel
client={client()}
name="trends"
prefetch={prefetching()}
/>
<div class="tab-panel">
<div>
<TimelinePanel
client={client()}
name="trends"
prefetch={prefetching()}
/>
</div>
</div>
</div>
<div class="tab-panel">
<div>
<TimelinePanel
client={client()}
name="public"
prefetch={prefetching()}
/>
<div class="tab-panel">
<div>
<TimelinePanel
client={client()}
name="public"
prefetch={prefetching()}
/>
</div>
</div>
<div></div>
</div>
<div></div>
</div>
</TimeSourceProvider>
</Scaffold>
</TimeSourceProvider>
<BottomSheet open={!!child()}>{child()}</BottomSheet>
</Scaffold>
</>
);
};

View file

@ -15,6 +15,7 @@ import {
Star as LikeIcon,
FeaturedPlayList as ListIcon,
} from "@suid/icons-material";
import { A } from "@solidjs/router";
const ProfileMenuButton: ParentComponent<{
profile?: { displayName: string; avatar: string; username: string };
@ -107,7 +108,7 @@ const ProfileMenuButton: ParentComponent<{
{props.children}
<Divider />
</Show>
<MenuItem>
<MenuItem component={A} href="/settings" onClick={onClose}>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>

View file

@ -3,6 +3,7 @@ import solid from "vite-plugin-solid";
import solidStyled from "vite-plugin-solid-styled";
import suid from "@suid/vite-plugin";
import { VitePWA } from "vite-plugin-pwa";
import version from "vite-plugin-package-version";
export default defineConfig(({ mode }) => ({
plugins: [
@ -17,7 +18,11 @@ export default defineConfig(({ mode }) => ({
VitePWA({
registerType: "autoUpdate",
}),
version(),
],
define: {
"import.meta.env.BUILT_AT": `"${new Date().toISOString()}"`,
},
css: {
devSourcemap: true,
},