added settings
This commit is contained in:
parent
b4f7a863a2
commit
71b9a60b35
15 changed files with 359 additions and 91 deletions
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
12
src/App.css
12
src/App.css
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
32
src/material/BottomSheet.module.css
Normal file
32
src/material/BottomSheet.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
26
src/material/BottomSheet.tsx
Normal file
26
src/material/BottomSheet.tsx
Normal 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;
|
|
@ -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
10
src/overrides.d.ts
vendored
Normal 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
137
src/settings/Settings.tsx
Normal 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;
|
|
@ -6,6 +6,8 @@ import {
|
|||
Show,
|
||||
untrack,
|
||||
onMount,
|
||||
type ParentComponent,
|
||||
children,
|
||||
} from "solid-js";
|
||||
import { useDocumentTitle } from "../utils";
|
||||
import { useSessions } from "../masto/clients";
|
||||
|
@ -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,6 +262,7 @@ const Home: Component = () => {
|
|||
`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Scaffold
|
||||
topbar={
|
||||
<AppBar position="static">
|
||||
|
@ -325,7 +327,9 @@ const Home: Component = () => {
|
|||
<div></div>
|
||||
</div>
|
||||
</TimeSourceProvider>
|
||||
<BottomSheet open={!!child()}>{child()}</BottomSheet>
|
||||
</Scaffold>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue