Compare commits
2 commits
b4f7a863a2
...
85ac9a236b
Author | SHA1 | Date | |
---|---|---|---|
|
85ac9a236b | ||
|
71b9a60b35 |
16 changed files with 364 additions and 91 deletions
|
@ -28,8 +28,13 @@ jobs:
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pnpm i
|
run: pnpm i
|
||||||
|
|
||||||
|
- name: Build Dist (Staging)
|
||||||
|
run: pnpm dist -m staging
|
||||||
|
if: env.GITHUB_REF_NAME == 'master'
|
||||||
|
|
||||||
- name: Build Dist
|
- name: Build Dist
|
||||||
run: pnpm dist
|
run: pnpm dist
|
||||||
|
if: env.GITHUB_REF_NAME != 'master'
|
||||||
|
|
||||||
- name: Depoly to Preview
|
- name: Depoly to Preview
|
||||||
uses: https://github.com/cloudflare/wrangler-action@v3
|
uses: https://github.com/cloudflare/wrangler-action@v3
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.3.2",
|
||||||
"typescript": "^5.5.2",
|
"typescript": "^5.5.2",
|
||||||
"vite": "^5.3.2",
|
"vite": "^5.3.2",
|
||||||
|
"vite-plugin-package-version": "^1.1.0",
|
||||||
"vite-plugin-pwa": "^0.20.0",
|
"vite-plugin-pwa": "^0.20.0",
|
||||||
"vite-plugin-solid": "^2.10.2",
|
"vite-plugin-solid": "^2.10.2",
|
||||||
"vite-plugin-solid-styled": "^0.11.1",
|
"vite-plugin-solid-styled": "^0.11.1",
|
||||||
|
|
|
@ -69,6 +69,9 @@ importers:
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.3.2
|
specifier: ^5.3.2
|
||||||
version: 5.3.2(@types/node@20.14.10)(lightningcss@1.25.1)(terser@5.31.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:
|
vite-plugin-pwa:
|
||||||
specifier: ^0.20.0
|
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)
|
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:
|
validate-html-nesting@1.2.2:
|
||||||
resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==}
|
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:
|
vite-plugin-pwa@0.20.0:
|
||||||
resolution: {integrity: sha512-/kDZyqF8KqoXRpMUQtR5Atri/7BWayW8Gp7Kz/4bfstsV6zSFTxjREbXZYL7zSuRL40HGA+o2hvUAFRmC+bL7g==}
|
resolution: {integrity: sha512-/kDZyqF8KqoXRpMUQtR5Atri/7BWayW8Gp7Kz/4bfstsV6zSFTxjREbXZYL7zSuRL40HGA+o2hvUAFRmC+bL7g==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
@ -5280,6 +5288,10 @@ snapshots:
|
||||||
|
|
||||||
validate-html-nesting@1.2.2: {}
|
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):
|
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:
|
dependencies:
|
||||||
debug: 4.3.5
|
debug: 4.3.5
|
||||||
|
|
12
src/App.css
12
src/App.css
|
@ -1,14 +1,4 @@
|
||||||
html,
|
:root {
|
||||||
body {
|
|
||||||
overflow: hidden;
|
|
||||||
height: 100vh;
|
|
||||||
height: 100dvh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root {
|
|
||||||
overflow: hidden hidden;
|
|
||||||
height: 100vh;
|
|
||||||
height: 100dvh;
|
|
||||||
background-color: var(--tutu-color-surface, transparent);
|
background-color: var(--tutu-color-surface, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,11 +22,15 @@ const AccountMastodonOAuth2Callback = lazy(
|
||||||
() => import("./accounts/MastodonOAuth2Callback.js"),
|
() => import("./accounts/MastodonOAuth2Callback.js"),
|
||||||
);
|
);
|
||||||
const TimelineHome = lazy(() => import("./timelines/Home.js"));
|
const TimelineHome = lazy(() => import("./timelines/Home.js"));
|
||||||
|
const Settings = lazy(() => import("./settings/Settings.js"));
|
||||||
|
|
||||||
const Routing: Component = () => {
|
const Routing: Component = () => {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<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={"/accounts"}>
|
||||||
<Route path={"/sign-in"} component={AccountSignIn} />
|
<Route path={"/sign-in"} component={AccountSignIn} />
|
||||||
<Route
|
<Route
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { persistentAtom } from "@nanostores/persistent";
|
import { persistentAtom } from "@nanostores/persistent";
|
||||||
import { createOAuthAPIClient, createRestAPIClient } from "masto";
|
import {
|
||||||
|
createOAuthAPIClient,
|
||||||
|
createRestAPIClient,
|
||||||
|
type mastodon,
|
||||||
|
} from "masto";
|
||||||
import { action } from "nanostores";
|
import { action } from "nanostores";
|
||||||
|
import { createMastoClientFor } from "../masto/clients";
|
||||||
|
|
||||||
export type Account = {
|
export type Account = {
|
||||||
site: string;
|
site: string;
|
||||||
|
@ -9,6 +14,8 @@ export type Account = {
|
||||||
tokenType: string;
|
tokenType: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
|
|
||||||
|
inf?: mastodon.v1.AccountCredentials;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const $accounts = persistentAtom<Account[]>("accounts", [], {
|
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 = {
|
export type RegisteredApp = {
|
||||||
site: string;
|
site: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
|
|
|
@ -1,10 +1,35 @@
|
||||||
import { Accessor, createResource } from "solid-js";
|
import { Accessor, createResource } from "solid-js";
|
||||||
import type { mastodon } from "masto";
|
import type { mastodon } from "masto";
|
||||||
|
import { useSessions } from "./clients";
|
||||||
|
import { updateAcctInf } from "../accounts/stores";
|
||||||
|
|
||||||
export function useAcctProfile(client: Accessor<mastodon.rest.Client>) {
|
export function useAcctProfile(client: Accessor<mastodon.rest.Client>) {
|
||||||
return createResource(client, (client) => {
|
return createResource(
|
||||||
return client.v1.accounts.verifyCredentials()
|
client,
|
||||||
}, {
|
(client) => {
|
||||||
name: "MastodonAccountProfile"
|
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;
|
return sessions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSessionsRw() {
|
function useSessionsRw() {
|
||||||
const store = useContext(Context);
|
const store = useContext(Context);
|
||||||
if (!store) {
|
if (!store) {
|
||||||
throw new TypeError("sessions are not provided");
|
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`
|
css`
|
||||||
.scaffold-content {
|
.scaffold-content {
|
||||||
--scaffold-topbar-height: ${(topbarSize.height?.toString() ?? 0) + "px"};
|
--scaffold-topbar-height: ${(topbarSize.height?.toString() ?? 0) + "px"};
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar {
|
.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,9 +6,11 @@ import {
|
||||||
Show,
|
Show,
|
||||||
untrack,
|
untrack,
|
||||||
onMount,
|
onMount,
|
||||||
|
type ParentComponent,
|
||||||
|
children,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { useDocumentTitle } from "../utils";
|
import { useDocumentTitle } from "../utils";
|
||||||
import { useSessions } from "../masto/clients";
|
import { useSessions } from "../masto/clients";
|
||||||
import { type mastodon } from "masto";
|
import { type mastodon } from "masto";
|
||||||
import Scaffold from "../material/Scaffold";
|
import Scaffold from "../material/Scaffold";
|
||||||
import {
|
import {
|
||||||
|
@ -32,6 +34,7 @@ import Tab from "../material/Tab";
|
||||||
import { Create as CreateTootIcon } from "@suid/icons-material";
|
import { Create as CreateTootIcon } from "@suid/icons-material";
|
||||||
import { useTimeline } from "../masto/timelines";
|
import { useTimeline } from "../masto/timelines";
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener";
|
import { makeEventListener } from "@solid-primitives/event-listener";
|
||||||
|
import BottomSheet from "../material/BottomSheet";
|
||||||
|
|
||||||
const TimelinePanel: Component<{
|
const TimelinePanel: Component<{
|
||||||
client: mastodon.rest.Client;
|
client: mastodon.rest.Client;
|
||||||
|
@ -145,7 +148,7 @@ const TimelinePanel: Component<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Home: Component = () => {
|
const Home: ParentComponent = (props) => {
|
||||||
let panelList: HTMLDivElement;
|
let panelList: HTMLDivElement;
|
||||||
useDocumentTitle("Timelines");
|
useDocumentTitle("Timelines");
|
||||||
const now = createTimeSource();
|
const now = createTimeSource();
|
||||||
|
@ -162,6 +165,8 @@ const Home: Component = () => {
|
||||||
number,
|
number,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const child = children(() => props.children)
|
||||||
|
|
||||||
let scrollEventLockReleased = true;
|
let scrollEventLockReleased = true;
|
||||||
|
|
||||||
const recalculateTabIndicator = () => {
|
const recalculateTabIndicator = () => {
|
||||||
|
@ -221,7 +226,7 @@ const Home: Component = () => {
|
||||||
const onTabClick = (idx: number) => {
|
const onTabClick = (idx: number) => {
|
||||||
const items = panelList.querySelectorAll(".tab-panel");
|
const items = panelList.querySelectorAll(".tab-panel");
|
||||||
if (items.length > idx) {
|
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));
|
max-height: calc(100dvh - var(--scaffold-topbar-height, 0px));
|
||||||
scroll-snap-align: center;
|
scroll-snap-align: center;
|
||||||
|
|
||||||
&:not(.active) {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
@ -261,71 +262,74 @@ const Home: Component = () => {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Scaffold
|
<>
|
||||||
topbar={
|
<Scaffold
|
||||||
<AppBar position="static">
|
topbar={
|
||||||
<Toolbar variant="dense" class="responsive">
|
<AppBar position="static">
|
||||||
<Tabs onFocusChanged={setCurrentFocusOn} offset={panelOffset()}>
|
<Toolbar variant="dense" class="responsive">
|
||||||
<Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
|
<Tabs onFocusChanged={setCurrentFocusOn} offset={panelOffset()}>
|
||||||
Home
|
<Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
|
||||||
</Tab>
|
Home
|
||||||
<Tab focus={isTabFocus(1)} onClick={[onTabClick, 1]}>
|
</Tab>
|
||||||
Trending
|
<Tab focus={isTabFocus(1)} onClick={[onTabClick, 1]}>
|
||||||
</Tab>
|
Trending
|
||||||
<Tab focus={isTabFocus(2)} onClick={[onTabClick, 2]}>
|
</Tab>
|
||||||
Public
|
<Tab focus={isTabFocus(2)} onClick={[onTabClick, 2]}>
|
||||||
</Tab>
|
Public
|
||||||
</Tabs>
|
</Tab>
|
||||||
<ProfileMenuButton profile={profile()}>
|
</Tabs>
|
||||||
<MenuItem onClick={(e) => setPrefetching((x) => !x)}>
|
<ProfileMenuButton profile={profile()}>
|
||||||
<ListItemText>Prefetch Toots</ListItemText>
|
<MenuItem onClick={(e) => setPrefetching((x) => !x)}>
|
||||||
<ListItemSecondaryAction>
|
<ListItemText>Prefetch Toots</ListItemText>
|
||||||
<Switch checked={prefetching()}></Switch>
|
<ListItemSecondaryAction>
|
||||||
</ListItemSecondaryAction>
|
<Switch checked={prefetching()}></Switch>
|
||||||
</MenuItem>
|
</ListItemSecondaryAction>
|
||||||
</ProfileMenuButton>
|
</MenuItem>
|
||||||
</Toolbar>
|
</ProfileMenuButton>
|
||||||
</AppBar>
|
</Toolbar>
|
||||||
}
|
</AppBar>
|
||||||
fab={
|
}
|
||||||
<Fab color="secondary">
|
fab={
|
||||||
<CreateTootIcon />
|
<Fab color="secondary">
|
||||||
</Fab>
|
<CreateTootIcon />
|
||||||
}
|
</Fab>
|
||||||
>
|
}
|
||||||
<TimeSourceProvider value={now}>
|
>
|
||||||
<div class="panel-list" ref={panelList!}>
|
<TimeSourceProvider value={now}>
|
||||||
<div class="tab-panel">
|
<div class="panel-list" ref={panelList!}>
|
||||||
<div>
|
<div class="tab-panel">
|
||||||
<TimelinePanel
|
<div>
|
||||||
client={client()}
|
<TimelinePanel
|
||||||
name="home"
|
client={client()}
|
||||||
prefetch={prefetching()}
|
name="home"
|
||||||
/>
|
prefetch={prefetching()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="tab-panel">
|
||||||
<div class="tab-panel">
|
<div>
|
||||||
<div>
|
<TimelinePanel
|
||||||
<TimelinePanel
|
client={client()}
|
||||||
client={client()}
|
name="trends"
|
||||||
name="trends"
|
prefetch={prefetching()}
|
||||||
prefetch={prefetching()}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="tab-panel">
|
||||||
<div class="tab-panel">
|
<div>
|
||||||
<div>
|
<TimelinePanel
|
||||||
<TimelinePanel
|
client={client()}
|
||||||
client={client()}
|
name="public"
|
||||||
name="public"
|
prefetch={prefetching()}
|
||||||
prefetch={prefetching()}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
<div></div>
|
</TimeSourceProvider>
|
||||||
</div>
|
<BottomSheet open={!!child()}>{child()}</BottomSheet>
|
||||||
</TimeSourceProvider>
|
</Scaffold>
|
||||||
</Scaffold>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
Star as LikeIcon,
|
Star as LikeIcon,
|
||||||
FeaturedPlayList as ListIcon,
|
FeaturedPlayList as ListIcon,
|
||||||
} from "@suid/icons-material";
|
} from "@suid/icons-material";
|
||||||
|
import { A } from "@solidjs/router";
|
||||||
|
|
||||||
const ProfileMenuButton: ParentComponent<{
|
const ProfileMenuButton: ParentComponent<{
|
||||||
profile?: { displayName: string; avatar: string; username: string };
|
profile?: { displayName: string; avatar: string; username: string };
|
||||||
|
@ -107,7 +108,7 @@ const ProfileMenuButton: ParentComponent<{
|
||||||
{props.children}
|
{props.children}
|
||||||
<Divider />
|
<Divider />
|
||||||
</Show>
|
</Show>
|
||||||
<MenuItem>
|
<MenuItem component={A} href="/settings" onClick={onClose}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import solid from "vite-plugin-solid";
|
||||||
import solidStyled from "vite-plugin-solid-styled";
|
import solidStyled from "vite-plugin-solid-styled";
|
||||||
import suid from "@suid/vite-plugin";
|
import suid from "@suid/vite-plugin";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
import version from "vite-plugin-package-version";
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -17,7 +18,11 @@ export default defineConfig(({ mode }) => ({
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: "autoUpdate",
|
registerType: "autoUpdate",
|
||||||
}),
|
}),
|
||||||
|
version(),
|
||||||
],
|
],
|
||||||
|
define: {
|
||||||
|
"import.meta.env.BUILT_AT": `"${new Date().toISOString()}"`,
|
||||||
|
},
|
||||||
css: {
|
css: {
|
||||||
devSourcemap: true,
|
devSourcemap: true,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue