From 65fa4c4fa5f4222f52dd0f6bf33c23bc5f926b40 Mon Sep 17 00:00:00 2001 From: thislight Date: Sun, 3 Nov 2024 20:50:31 +0800 Subject: [PATCH] Profile: add subscribe menu --- src/material/Menu.tsx | 26 +++++-- src/material/Scaffold.css | 22 ++++++ src/material/Scaffold.tsx | 85 ++++++++++---------- src/material/theme.css | 2 + src/profiles/Profile.css | 82 +++++++++++++++++++ src/profiles/Profile.tsx | 160 +++++++++++++------------------------- 6 files changed, 226 insertions(+), 151 deletions(-) create mode 100644 src/material/Scaffold.css create mode 100644 src/profiles/Profile.css diff --git a/src/material/Menu.tsx b/src/material/Menu.tsx index a80a423..355cc03 100644 --- a/src/material/Menu.tsx +++ b/src/material/Menu.tsx @@ -3,8 +3,9 @@ import { MenuList } from "@suid/material"; import { createEffect, createSignal, + type Component, type JSX, - type ParentComponent, + type ParentProps, } from "solid-js"; import { ANIM_CURVE_STD } from "./theme"; import "./Menu.css"; @@ -13,13 +14,13 @@ import { animateShrinkToTopRight, } from "../platform/anim"; -type Anchor = Pick +export type Anchor = Pick -type Props = { +export type MenuProps = ParentProps<{ open?: boolean; onClose?: JSX.EventHandlerUnion; anchor: () => Anchor; -}; +}>; function px(n?: number) { if (n) { @@ -29,7 +30,22 @@ function px(n?: number) { } } -const Menu: ParentComponent = (props) => { +export function createManagedMenuState() { + const [anchor, setAnchor] = createSignal(); + + return [ + setAnchor, + { + get open() { + return !!anchor(); + }, + anchor: anchor as () => Anchor, + onClose: () => setAnchor(), + }, + ] as const; +} + +const Menu: Component = (props) => { let root: HTMLDialogElement; const windowSize = useWindowSize(); diff --git a/src/material/Scaffold.css b/src/material/Scaffold.css new file mode 100644 index 0000000..fdcf2bd --- /dev/null +++ b/src/material/Scaffold.css @@ -0,0 +1,22 @@ + +.Scaffold__topbar { + position: sticky; + top: 0px; + z-index: var(--tutu-zidx-nav, auto); +} + +.Scaffold__fab-dock { + position: fixed; + bottom: 40px; + right: 40px; + z-index: var(--tutu-zidx-nav, auto); +} + +.Scaffold__bottom-dock { + position: sticky; + bottom: 0; + left: 0; + right: 0; + z-index: var(--tutu-zidx-nav, auto); + padding-bottom: var(--safe-area-inset-bottom, 0); +} \ No newline at end of file diff --git a/src/material/Scaffold.tsx b/src/material/Scaffold.tsx index cebcda7..fa9631f 100644 --- a/src/material/Scaffold.tsx +++ b/src/material/Scaffold.tsx @@ -1,66 +1,67 @@ import { createElementSize } from "@solid-primitives/resize-observer"; import { + JSX, Show, createRenderEffect, createSignal, - onCleanup, - type JSX, - type ParentComponent, + splitProps, + type Component, + type ParentProps, } from "solid-js"; -import { css } from "solid-styled"; +import "./Scaffold.css"; -interface ScaffoldProps { - topbar?: JSX.Element; - fab?: JSX.Element; - bottom?: JSX.Element; -} +type ScaffoldProps = ParentProps< + { + topbar?: JSX.Element; + fab?: JSX.Element; + bottom?: JSX.Element; + } & JSX.HTMLElementTags["div"] +>; -const Scaffold: ParentComponent = (props) => { +/** + * The passthrough props are passed to the content container. + */ +const Scaffold: Component = (props) => { + const [managed, rest] = splitProps(props, [ + "topbar", + "fab", + "bottom", + "children", + "ref", + ]); const [topbarElement, setTopbarElement] = createSignal(); const topbarSize = createElementSize(topbarElement); - css` - .scaffold-content { - --scaffold-topbar-height: ${(topbarSize.height?.toString() ?? 0) + "px"}; - } - - .topbar { - position: sticky; - top: 0px; - z-index: var(--tutu-zidx-nav, auto); - } - - .fab-dock { - position: fixed; - bottom: 40px; - right: 40px; - z-index: var(--tutu-zidx-nav, auto); - } - - .bottom-dock { - position: sticky; - bottom: 0; - left: 0; - right: 0; - z-index: var(--tutu-zidx-nav, auto); - padding-bottom: var(--safe-area-inset-bottom, 0); - - } - `; return ( <> -
+
{props.topbar}
-
{props.fab}
+
{props.fab}
-
{props.children}
+
{ + createRenderEffect(() => { + e.style.setProperty( + "--scaffold-topbar-height", + (topbarSize.height?.toString() ?? 0) + "px", + ); + }); + + if (managed.ref) { + (managed.ref as (val: typeof e) => void)(e); + } + }} + {...rest} + > + {managed.children} +
-
{props.bottom}
+
{props.bottom}
); diff --git a/src/material/theme.css b/src/material/theme.css index 09cc746..5048f98 100644 --- a/src/material/theme.css +++ b/src/material/theme.css @@ -106,6 +106,8 @@ /* Submenu (+1dp for each submenu) */ --tutu-shadow-e9: 0px 9px 18px 0px var(--tutu-color-shadow); + --tutu-shadow-e10: 0px 10px 18px 0px var(--tutu-color-shadow); + --tutu-shadow-e11: 0px 11px 18px 0px var(--tutu-color-shadow-l1); /* (pressed) FAB */ --tutu-shadow-e12: 0px 12px 24px 0px var(--tutu-color-shadow-l1); diff --git a/src/profiles/Profile.css b/src/profiles/Profile.css new file mode 100644 index 0000000..31e5c78 --- /dev/null +++ b/src/profiles/Profile.css @@ -0,0 +1,82 @@ +.Profile { + .intro { + background-color: var(--tutu-color-surface-d); + color: var(--tutu-color-on-surface); + padding: 16px 12px; + display: flex; + flex-flow: column nowrap; + gap: 16px; + } + + .description { + & a { + color: inherit; + font-style: italic; + } + + word-break: break-all; + } + + .acct-grp { + display: flex; + flex-flow: row wrap; + gap: 16px; + align-items: center; + + & > :nth-child(2) { + flex-grow: 1; + } + + & > :last-child { + flex-grow: 1; + text-align: right; + } + } + + .name-grp { + display: flex; + flex-flow: column nowrap; + } + + table.acct-fields { + word-break: break-all; + + & td > a { + display: inline-flex; + align-items: center; + color: inherit; + min-height: 44px; + } + + & a > .invisible { + display: none; + } + + & svg { + vertical-align: middle; + } + } + + .toot-list-toolbar { + position: sticky; + top: var(--scaffold-topbar-height); + z-index: calc(var(--tutu-zidx-nav, 1) - 1); + background: var(--tutu-color-surface); + border-bottom: 1px solid var(--tutu-color-surface-d); + contain: content; + /* TODO: box-shadow is needed here (same as app bar, e6). + There is no good way to detect if the sticky is "sticked" - + so let's leave it for future. + + For now we use a trick to make it looks better. + */ + box-shadow: 0px -2px 4px 0px var(--tutu-color-shadow); + } +} + +.Profile__page-title { + flex-grow: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/src/profiles/Profile.tsx b/src/profiles/Profile.tsx index 4becc5b..8a6e6a1 100644 --- a/src/profiles/Profile.tsx +++ b/src/profiles/Profile.tsx @@ -5,6 +5,8 @@ import { createSignal, createUniqueId, For, + Switch, + Match, onCleanup, Show, type Component, @@ -42,13 +44,13 @@ import { useSessionForAcctStr } from "../masto/clients"; import { resolveCustomEmoji } from "../masto/toot"; import { FastAverageColor } from "fast-average-color"; import { useWindowSize } from "@solid-primitives/resize-observer"; -import { css } from "solid-styled"; import { createTimeline, createTimelineSnapshot } from "../masto/timelines"; import TootList from "../timelines/TootList"; import { createTimeSource, TimeSourceProvider } from "../platform/timesrc"; import TootFilterButton from "./TootFilterButton"; -import Menu from "../material/Menu"; +import Menu, { createManagedMenuState } from "../material/Menu"; import { share } from "../platform/share"; +import "./Profile.css"; const Profile: Component = () => { const navigate = useNavigate(); @@ -65,6 +67,10 @@ const Profile: Component = () => { const menuButId = createUniqueId(); const [menuOpen, setMenuOpen] = createSignal(false); + const [subscribeMenuOpen, setSubscribeMenuOpen] = createSignal(false); + let subcribeMenuAnchor: { top: number; right: number; left: number }; + + const [openSubscribeMenu, subscribeMenuState] = createManagedMenuState(); const [scrolledPastBanner, setScrolledPastBanner] = createSignal(false); const obx = new IntersectionObserver( @@ -132,89 +138,6 @@ const Profile: Component = () => { recentTootChunk.loading || (recentTootFilter().pinned && pinnedTootChunk.loading); - css` - .intro { - background-color: var(--tutu-color-surface-d); - color: var(--tutu-color-on-surface); - padding: 16px 12px; - display: flex; - flex-flow: column nowrap; - gap: 16px; - } - - .description { - & :global(a) { - color: inherit; - font-style: italic; - } - - word-break: break-all; - } - - .acct-grp { - display: flex; - flex-flow: row wrap; - gap: 16px; - align-items: center; - - & > :nth-child(2) { - flex-grow: 1; - } - - & > :last-child { - flex-grow: 1; - text-align: right; - } - } - - .name-grp { - display: flex; - flex-flow: column nowrap; - } - - table.acct-fields { - word-break: break-all; - - & td > :global(a) { - display: inline-flex; - align-items: center; - color: inherit; - min-height: 44px; - } - - & :global(a > .invisible) { - display: none; - } - - & :global(svg) { - vertical-align: middle; - } - } - - .page-title { - flex-grow: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .toot-list-toolbar { - position: sticky; - top: var(--scaffold-topbar-height); - z-index: calc(var(--tutu-zidx-nav, 1) - 1); - background: var(--tutu-color-surface); - border-bottom: 1px solid var(--tutu-color-surface-d); - contain: content; - /* TODO: box-shadow is needed here (same as app bar, e6). - There is no good way to detect if the sticky is "sticked" - - so let's leave it for future. - - For now we use a trick to make it looks better. - */ - box-shadow: 0px -2px 4px 0px var(--tutu-color-shadow); - } - `; - return ( { { </Toolbar> </AppBar> } + class="Profile" > <Menu open={menuOpen()} @@ -269,25 +192,27 @@ const Profile: Component = () => { document.getElementById(menuButId)!.getBoundingClientRect() } > - <Show - when={isCurrentSessionProfile()} - fallback={ + <Show when={session().account}> + <Show + when={isCurrentSessionProfile()} + fallback={ + <MenuItem disabled> + <ListItemIcon> + <PlaylistAdd /> + </ListItemIcon> + <ListItemText>Subscribe...</ListItemText> + </MenuItem> + } + > <MenuItem disabled> <ListItemIcon> - <PlaylistAdd /> + <Edit /> </ListItemIcon> - <ListItemText>Subscribe...</ListItemText> + <ListItemText>Edit...</ListItemText> </MenuItem> - } - > - <MenuItem disabled> - <ListItemIcon> - <Edit /> - </ListItemIcon> - <ListItemText>Edit...</ListItemText> - </MenuItem> + </Show> + <Divider /> </Show> - <Divider /> <MenuItem disabled> <ListItemIcon> <Group /> @@ -360,6 +285,15 @@ const Profile: Component = () => { ></img> </div> + <Menu {...subscribeMenuState}> + <MenuItem disabled> + <ListItemText> + <span>{session().account?.inf?.displayName || ""}</span> + <span>'s timeline</span> + </ListItemText> + </MenuItem> + </Menu> + <div class="intro" style={{ @@ -385,9 +319,27 @@ const Profile: Component = () => { <span>{fullUsername()}</span> </div> <div> - <Button variant="contained" color="secondary"> - Subscribe - </Button> + <Switch> + <Match when={!session().account}>{<></>}</Match> + <Match when={isCurrentSessionProfile()}> + <IconButton color="inherit"> + <Edit /> + </IconButton> + </Match> + <Match when={true}> + <Button + variant="contained" + color="secondary" + onClick={(event) => { + openSubscribeMenu( + event.currentTarget.getBoundingClientRect(), + ); + }} + > + Subscribe + </Button> + </Match> + </Switch> </div> </div> <div