BottomSheet: backward animation
This commit is contained in:
		
							parent
							
								
									0d856c61c7
								
							
						
					
					
						commit
						c461ae72f8
					
				
					 9 changed files with 150 additions and 75 deletions
				
			
		|  | @ -3,18 +3,6 @@ import type { mastodon } from "masto"; | ||||||
| import { useSessions } from "./clients"; | import { useSessions } from "./clients"; | ||||||
| import { updateAcctInf } from "../accounts/stores"; | import { updateAcctInf } from "../accounts/stores"; | ||||||
| 
 | 
 | ||||||
| export function useAcctProfile(client: Accessor<mastodon.rest.Client>) { |  | ||||||
|   return createResource( |  | ||||||
|     client, |  | ||||||
|     (client) => { |  | ||||||
|       return client.v1.accounts.verifyCredentials(); |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: "MastodonAccountProfile", |  | ||||||
|     }, |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function useSignedInProfiles() { | export function useSignedInProfiles() { | ||||||
|   const sessions = useSessions(); |   const sessions = useSessions(); | ||||||
|   const [accessor, tools] = createResource(sessions, async (all) => { |   const [accessor, tools] = createResource(sessions, async (all) => { | ||||||
|  | @ -24,11 +12,11 @@ export function useSignedInProfiles() { | ||||||
|   }); |   }); | ||||||
|   return [ |   return [ | ||||||
|     () => { |     () => { | ||||||
|       if (accessor.loading) { |       const value = accessor(); | ||||||
|         accessor(); |       if (!value) { | ||||||
|         return sessions().map((x) => ({ ...x, inf: x.account.inf })); |         return sessions().map((x) => ({ ...x, inf: x.account.inf })); | ||||||
|       } |       } | ||||||
|       return accessor(); |       return value; | ||||||
|     }, |     }, | ||||||
|     tools, |     tools, | ||||||
|   ] as const; |   ] as const; | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | import { cache } from "@solidjs/router"; | ||||||
| import type { mastodon } from "masto"; | import type { mastodon } from "masto"; | ||||||
| import { createRenderEffect, createResource, type Accessor } from "solid-js"; | import { createRenderEffect, createResource, type Accessor } from "solid-js"; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -11,10 +11,14 @@ | ||||||
|   border-radius: 2px; |   border-radius: 2px; | ||||||
|   overscroll-behavior: contain; |   overscroll-behavior: contain; | ||||||
| 
 | 
 | ||||||
|  |   &::backdrop { | ||||||
|  |     background-color: black; | ||||||
|  |     opacity: 0.5; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   box-shadow: var(--tutu-shadow-e16); |   box-shadow: var(--tutu-shadow-e16); | ||||||
| 
 | 
 | ||||||
|   :global(.MuiToolbar-root) > :global(.MuiButtonBase-root):first-child { |   :global(.MuiToolbar-root) > :global(.MuiButtonBase-root):first-child { | ||||||
|     color: white; |  | ||||||
|     margin-left: -0.5em; |     margin-left: -0.5em; | ||||||
|     margin-right: 24px; |     margin-right: 24px; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -3,6 +3,8 @@ import { | ||||||
|   createRenderEffect, |   createRenderEffect, | ||||||
|   onCleanup, |   onCleanup, | ||||||
|   onMount, |   onMount, | ||||||
|  |   startTransition, | ||||||
|  |   useTransition, | ||||||
|   type ParentComponent, |   type ParentComponent, | ||||||
| } from "solid-js"; | } from "solid-js"; | ||||||
| import styles from "./BottomSheet.module.css"; | import styles from "./BottomSheet.module.css"; | ||||||
|  | @ -14,17 +16,21 @@ export type BottomSheetProps = { | ||||||
| 
 | 
 | ||||||
| export const HERO = Symbol("BottomSheet Hero Symbol"); | export const HERO = Symbol("BottomSheet Hero Symbol"); | ||||||
| 
 | 
 | ||||||
| function composeAnimationFrame({ | function composeAnimationFrame( | ||||||
|  |   { | ||||||
|     top, |     top, | ||||||
|     left, |     left, | ||||||
|     height, |     height, | ||||||
|     width, |     width, | ||||||
| }: Record<"top" | "left" | "height" | "width", number>) { |   }: Record<"top" | "left" | "height" | "width", number>, | ||||||
|  |   x: Record<string, unknown>, | ||||||
|  | ) { | ||||||
|   return { |   return { | ||||||
|     top: `${top}px`, |     top: `${top}px`, | ||||||
|     left: `${left}px`, |     left: `${left}px`, | ||||||
|     height: `${height}px`, |     height: `${height}px`, | ||||||
|     width: `${width}px`, |     width: `${width}px`, | ||||||
|  |     ...x, | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -35,30 +41,50 @@ const BottomSheet: ParentComponent<BottomSheetProps> = (props) => { | ||||||
|   let animation: Animation | undefined; |   let animation: Animation | undefined; | ||||||
|   const hero = useHeroSignal(HERO); |   const hero = useHeroSignal(HERO); | ||||||
| 
 | 
 | ||||||
|  |   const [pending] = useTransition() | ||||||
|  | 
 | ||||||
|   createEffect(() => { |   createEffect(() => { | ||||||
|     if (props.open) { |     if (props.open) { | ||||||
|       if (!element.open) { |       if (!element.open && !pending()) { | ||||||
|         element.showModal(); |         animatedOpen(); | ||||||
|         animateOpen(); |  | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|       if (element.open) { |       if (element.open) { | ||||||
|         if (animation) { |         animatedClose(); | ||||||
|           animation.cancel(); |  | ||||||
|         } |  | ||||||
|         element.close(); |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   const animateOpen = () => { |   const animatedClose = () => { | ||||||
|     // Do hero animation
 |     const endRect = hero(); | ||||||
|  |     if (endRect) { | ||||||
|  |       const startRect = element.getBoundingClientRect(); | ||||||
|  |       const animation = animateHero(startRect, endRect, element, true); | ||||||
|  |       const onClose = () => { | ||||||
|  |         element.close(); | ||||||
|  |       }; | ||||||
|  |       animation.addEventListener("finish", onClose); | ||||||
|  |       animation.addEventListener("cancel", onClose); | ||||||
|  |     } else { | ||||||
|  |       element.close(); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const animatedOpen = () => { | ||||||
|  |     element.showModal(); | ||||||
|     const startRect = hero(); |     const startRect = hero(); | ||||||
|     console.debug("capture hero source", startRect); |  | ||||||
|     if (!startRect) return; |     if (!startRect) return; | ||||||
|     const endRect = element.getBoundingClientRect(); |     const endRect = element.getBoundingClientRect(); | ||||||
|     const easing = "ease-in-out"; |     animateHero(startRect, endRect, element); | ||||||
|     console.debug("easing", easing); |   }; | ||||||
|  | 
 | ||||||
|  |   const animateHero = ( | ||||||
|  |     startRect: DOMRect, | ||||||
|  |     endRect: DOMRect, | ||||||
|  |     element: HTMLElement, | ||||||
|  |     reserve?: boolean, | ||||||
|  |   ) => { | ||||||
|  |     const easing = "cubic-bezier(0.4, 0, 0.2, 1)"; | ||||||
|     element.classList.add(styles.animated); |     element.classList.add(styles.animated); | ||||||
|     const distance = Math.sqrt( |     const distance = Math.sqrt( | ||||||
|       Math.pow(Math.abs(startRect.top - endRect.top), 2) + |       Math.pow(Math.abs(startRect.top - endRect.top), 2) + | ||||||
|  | @ -66,7 +92,10 @@ const BottomSheet: ParentComponent<BottomSheetProps> = (props) => { | ||||||
|     ); |     ); | ||||||
|     const duration = (distance / MOVE_SPEED) * 1000; |     const duration = (distance / MOVE_SPEED) * 1000; | ||||||
|     animation = element.animate( |     animation = element.animate( | ||||||
|       [composeAnimationFrame(startRect), composeAnimationFrame(endRect)], |       [ | ||||||
|  |         composeAnimationFrame(startRect, { opacity: reserve ? 1 : 0.5 }), | ||||||
|  |         composeAnimationFrame(endRect, { opacity: reserve ? 0.5 : 1 }), | ||||||
|  |       ], | ||||||
|       { easing, duration }, |       { easing, duration }, | ||||||
|     ); |     ); | ||||||
|     const onAnimationEnd = () => { |     const onAnimationEnd = () => { | ||||||
|  | @ -75,6 +104,7 @@ const BottomSheet: ParentComponent<BottomSheetProps> = (props) => { | ||||||
|     }; |     }; | ||||||
|     animation.addEventListener("finish", onAnimationEnd); |     animation.addEventListener("finish", onAnimationEnd); | ||||||
|     animation.addEventListener("cancel", onAnimationEnd); |     animation.addEventListener("cancel", onAnimationEnd); | ||||||
|  |     return animation; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   onCleanup(() => { |   onCleanup(() => { | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ export type HeroSource = { | ||||||
|   [key: string | symbol | number]: DOMRect | undefined; |   [key: string | symbol | number]: DOMRect | undefined; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const HeroSourceContext = createContext<Signal<HeroSource>>(undefined); | const HeroSourceContext = createContext<Signal<HeroSource>>(/* __@PURE__ */undefined); | ||||||
| 
 | 
 | ||||||
| export const HeroSourceProvider = HeroSourceContext.Provider; | export const HeroSourceProvider = HeroSourceContext.Provider; | ||||||
| 
 | 
 | ||||||
|  | @ -20,6 +20,9 @@ function useHeroSource() { | ||||||
|   return useContext(HeroSourceContext); |   return useContext(HeroSourceContext); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Use hero value for the {@link key}. | ||||||
|  |  */ | ||||||
| export function useHeroSignal( | export function useHeroSignal( | ||||||
|   key: string | symbol | number, |   key: string | symbol | number, | ||||||
| ): Accessor<DOMRect | undefined> { | ): Accessor<DOMRect | undefined> { | ||||||
|  | @ -29,7 +32,6 @@ export function useHeroSignal( | ||||||
| 
 | 
 | ||||||
|     createRenderEffect(() => { |     createRenderEffect(() => { | ||||||
|       const value = source[0](); |       const value = source[0](); | ||||||
|       console.debug("value", value); |  | ||||||
|       if (value[key]) { |       if (value[key]) { | ||||||
|         set(value[key]); |         set(value[key]); | ||||||
|         source[1]((x) => { |         source[1]((x) => { | ||||||
|  |  | ||||||
|  | @ -50,7 +50,7 @@ const Settings: ParentComponent = () => { | ||||||
|       topbar={ |       topbar={ | ||||||
|         <AppBar position="static"> |         <AppBar position="static"> | ||||||
|           <Toolbar variant="dense" sx={{paddingTop: "var(--safe-area-inset-top, 0px)"}}> |           <Toolbar variant="dense" sx={{paddingTop: "var(--safe-area-inset-top, 0px)"}}> | ||||||
|             <IconButton onClick={[navigate, -1]}> |             <IconButton color="inherit" onClick={[navigate, -1]}> | ||||||
|               <CloseIcon /> |               <CloseIcon /> | ||||||
|             </IconButton> |             </IconButton> | ||||||
|             <Title>Settings</Title> |             <Title>Settings</Title> | ||||||
|  |  | ||||||
|  | @ -8,9 +8,9 @@ import { | ||||||
|   onMount, |   onMount, | ||||||
|   type ParentComponent, |   type ParentComponent, | ||||||
|   children, |   children, | ||||||
|  |   Suspense, | ||||||
| } from "solid-js"; | } from "solid-js"; | ||||||
| import { useDocumentTitle } from "../utils"; | import { useDocumentTitle } from "../utils"; | ||||||
| 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 { | ||||||
|  | @ -27,7 +27,6 @@ import { | ||||||
| import { css } from "solid-styled"; | import { css } from "solid-styled"; | ||||||
| import { TimeSourceProvider, createTimeSource } from "../platform/timesrc"; | import { TimeSourceProvider, createTimeSource } from "../platform/timesrc"; | ||||||
| import TootThread from "./TootThread.js"; | import TootThread from "./TootThread.js"; | ||||||
| import { useAcctProfile } from "../masto/acct"; |  | ||||||
| import ProfileMenuButton from "./ProfileMenuButton"; | import ProfileMenuButton from "./ProfileMenuButton"; | ||||||
| import Tabs from "../material/Tabs"; | import Tabs from "../material/Tabs"; | ||||||
| import Tab from "../material/Tab"; | import Tab from "../material/Tab"; | ||||||
|  | @ -43,6 +42,7 @@ import { vibrate } from "../platform/hardware"; | ||||||
| import PullDownToRefresh from "./PullDownToRefresh"; | import PullDownToRefresh from "./PullDownToRefresh"; | ||||||
| import { HeroSourceProvider, type HeroSource } from "../platform/anim"; | import { HeroSourceProvider, type HeroSource } from "../platform/anim"; | ||||||
| import { useNavigate } from "@solidjs/router"; | import { useNavigate } from "@solidjs/router"; | ||||||
|  | import { useSignedInProfiles } from "../masto/acct"; | ||||||
| 
 | 
 | ||||||
| const TimelinePanel: Component<{ | const TimelinePanel: Component<{ | ||||||
|   client: mastodon.rest.Client; |   client: mastodon.rest.Client; | ||||||
|  | @ -195,9 +195,10 @@ const Home: ParentComponent = (props) => { | ||||||
|   const now = createTimeSource(); |   const now = createTimeSource(); | ||||||
| 
 | 
 | ||||||
|   const settings$ = useStore($settings); |   const settings$ = useStore($settings); | ||||||
|   const sessions = useSessions(); | 
 | ||||||
|   const client = () => sessions()[0].client; |   const [profiles] = useSignedInProfiles(); | ||||||
|   const [profile] = useAcctProfile(client); |   const profile = () => profiles()[0].inf; | ||||||
|  |   const client = () => profiles()[0].client; | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
| 
 | 
 | ||||||
|   const [heroSrc, setHeroSrc] = createSignal<HeroSource>({}); |   const [heroSrc, setHeroSrc] = createSignal<HeroSource>({}); | ||||||
|  | @ -278,10 +279,10 @@ const Home: ParentComponent = (props) => { | ||||||
|     toot: mastodon.v1.Status, |     toot: mastodon.v1.Status, | ||||||
|     srcElement?: HTMLElement, |     srcElement?: HTMLElement, | ||||||
|   ) => { |   ) => { | ||||||
|     const p = sessions()[0]; |     const p = profiles()[0]; | ||||||
|     const inf = p.account.inf ?? profile(); |     const inf = p.account.inf ?? profile(); | ||||||
|     if (!inf) { |     if (!inf) { | ||||||
|       console.warn('no account info?') |       console.warn("no account info?"); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     const rect = srcElement?.getBoundingClientRect(); |     const rect = srcElement?.getBoundingClientRect(); | ||||||
|  | @ -402,9 +403,11 @@ const Home: ParentComponent = (props) => { | ||||||
|             <div></div> |             <div></div> | ||||||
|           </div> |           </div> | ||||||
|         </TimeSourceProvider> |         </TimeSourceProvider> | ||||||
|  |         <Suspense> | ||||||
|           <HeroSourceProvider value={[heroSrc, setHeroSrc]}> |           <HeroSourceProvider value={[heroSrc, setHeroSrc]}> | ||||||
|             <BottomSheet open={!!child()}>{child()}</BottomSheet> |             <BottomSheet open={!!child()}>{child()}</BottomSheet> | ||||||
|           </HeroSourceProvider> |           </HeroSourceProvider> | ||||||
|  |         </Suspense> | ||||||
|       </Scaffold> |       </Scaffold> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  | @ -2,9 +2,7 @@ import type { mastodon } from "masto"; | ||||||
| import { type Component, For, createSignal } from "solid-js"; | import { type Component, For, createSignal } from "solid-js"; | ||||||
| import { css } from "solid-styled"; | import { css } from "solid-styled"; | ||||||
| import tootStyle from "./toot.module.css"; | import tootStyle from "./toot.module.css"; | ||||||
| import { Portal } from "solid-js/web"; | import MediaViewer from "./MediaViewer"; | ||||||
| import MediaViewer, { MEDIA_VIEWER_HEROSRC } from "./MediaViewer"; |  | ||||||
| import { HeroSourceProvider } from "../platform/anim"; |  | ||||||
| 
 | 
 | ||||||
| const MediaAttachmentGrid: Component<{ | const MediaAttachmentGrid: Component<{ | ||||||
|   attachments: mastodon.v1.MediaAttachment[]; |   attachments: mastodon.v1.MediaAttachment[]; | ||||||
|  | @ -58,13 +56,6 @@ const MediaAttachmentGrid: Component<{ | ||||||
|           } |           } | ||||||
|         }} |         }} | ||||||
|       </For> |       </For> | ||||||
|       <HeroSourceProvider |  | ||||||
|         value={() => ({ |  | ||||||
|           [MEDIA_VIEWER_HEROSRC]: rootRef.children.item( |  | ||||||
|             viewerIndex() || 0, |  | ||||||
|           ) as HTMLElement, |  | ||||||
|         })} |  | ||||||
|       > |  | ||||||
|         <MediaViewer |         <MediaViewer | ||||||
|           show={viewerOpened()} |           show={viewerOpened()} | ||||||
|           index={viewerIndex() || 0} |           index={viewerIndex() || 0} | ||||||
|  | @ -72,7 +63,6 @@ const MediaAttachmentGrid: Component<{ | ||||||
|           media={props.attachments} |           media={props.attachments} | ||||||
|           onClose={() => setViewerIndex(undefined)} |           onClose={() => setViewerIndex(undefined)} | ||||||
|         /> |         /> | ||||||
|       </HeroSourceProvider> |  | ||||||
|     </section> |     </section> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,23 +1,80 @@ | ||||||
| import { useParams } from "@solidjs/router"; | import { useNavigate, useParams } from "@solidjs/router"; | ||||||
| import type { Component } from "solid-js"; | import { createResource, Show, type Component } from "solid-js"; | ||||||
| import Scaffold from "../material/Scaffold"; | import Scaffold from "../material/Scaffold"; | ||||||
| import TootThread from "./TootThread"; | import TootThread from "./TootThread"; | ||||||
| import { AppBar, Toolbar } from "@suid/material"; | import { AppBar, IconButton, Toolbar } from "@suid/material"; | ||||||
| import { Title } from "../material/typography"; | import { Title } from "../material/typography"; | ||||||
|  | import { Close as CloseIcon } from "@suid/icons-material"; | ||||||
|  | import { isiOS } from "../platform/host"; | ||||||
|  | import { createUnauthorizedClient, useSessions } from "../masto/clients"; | ||||||
|  | import { resolveCustomEmoji } from "../masto/toot"; | ||||||
|  | import RegularToot from "./RegularToot"; | ||||||
| 
 | 
 | ||||||
| const TootBottomSheet: Component = (props) => { | const TootBottomSheet: Component = (props) => { | ||||||
|   const params = useParams() |   const params = useParams<{ acct: string; id: string }>(); | ||||||
|   return <Scaffold |   const navigate = useNavigate(); | ||||||
|  |   const allSession = useSessions(); | ||||||
|  |   const session = () => { | ||||||
|  |     const [inputUsername, inputSite] = decodeURIComponent(params.acct).split( | ||||||
|  |       "@", | ||||||
|  |       2, | ||||||
|  |     ); | ||||||
|  |     const authedSession = allSession().find( | ||||||
|  |       (x) => | ||||||
|  |         x.account.site === inputSite && | ||||||
|  |         x.account.inf?.username === inputUsername, | ||||||
|  |     ); | ||||||
|  |     return authedSession ?? { client: createUnauthorizedClient(inputSite) }; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const [remoteToot] = createResource( | ||||||
|  |     () => [session().client, params.id] as const, | ||||||
|  |     async ([client, id]) => { | ||||||
|  |       return await client.v1.statuses.$select(id).fetch(); | ||||||
|  |     }, | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const toot = remoteToot; | ||||||
|  | 
 | ||||||
|  |   const tootTitle = () => { | ||||||
|  |     const t = toot(); | ||||||
|  |     if (t) { | ||||||
|  |       const name = resolveCustomEmoji(t.account.displayName, t.account.emojis); | ||||||
|  |       return `${name}'s toot`; | ||||||
|  |     } | ||||||
|  |     return "A toot"; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <Scaffold | ||||||
|       topbar={ |       topbar={ | ||||||
|     <AppBar position="static"> |         <AppBar | ||||||
|       <Toolbar variant="dense" sx={{paddingTop: "var(--safe-area-inset-top, 0px)"}}> |           sx={{ | ||||||
|         <Title>A Toot</Title> |             backgroundColor: "var(--tutu-color-surface)", | ||||||
|  |             color: "var(--tutu-color-on-surface)", | ||||||
|  |           }} | ||||||
|  |           elevation={1} | ||||||
|  |           position="static" | ||||||
|  |         > | ||||||
|  |           <Toolbar | ||||||
|  |             variant="dense" | ||||||
|  |             sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }} | ||||||
|  |           > | ||||||
|  |             <IconButton color="inherit" onClick={[navigate, -1]} disableRipple> | ||||||
|  |               <CloseIcon /> | ||||||
|  |             </IconButton> | ||||||
|  |             <Title>{tootTitle}</Title> | ||||||
|           </Toolbar> |           </Toolbar> | ||||||
|         </AppBar> |         </AppBar> | ||||||
|       } |       } | ||||||
|     > |     > | ||||||
|     <p>{params.acct}/{params.id}</p> |       <div> | ||||||
|   </Scaffold>; |         <Show when={toot()}> | ||||||
|  |           <RegularToot status={toot()!}></RegularToot> | ||||||
|  |         </Show> | ||||||
|  |       </div> | ||||||
|  |     </Scaffold> | ||||||
|  |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default TootBottomSheet; | export default TootBottomSheet; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue