diff --git a/src/platform/StackedRouter.tsx b/src/platform/StackedRouter.tsx index 0686c9b..eab70ea 100644 --- a/src/platform/StackedRouter.tsx +++ b/src/platform/StackedRouter.tsx @@ -79,11 +79,6 @@ export type Navigator> = { const NavigatorContext = /* @__PURE__ */ createContext(); -/** - * Get the possible navigator of the {@link StackedRouter}. - * - * @see useNavigator for the navigator usage. - */ export function useMaybeNavigator() { return useContext(NavigatorContext); } @@ -96,8 +91,6 @@ export function useMaybeNavigator() { * path and its state. If you need push guide, you may want to * define your own function (like `useAppNavigator`) and cast the * navigator to the type you need. - * - * @see {@link useMaybeNavigator} if you are not sure you are under a {@link StackedRouter}. */ export function useNavigator() { const navigator = useMaybeNavigator(); @@ -117,20 +110,10 @@ export type CurrentFrame = { const CurrentFrameContext = /* @__PURE__ */ createContext>>(); -/** - * Return the current, if possible. - * - * @see {@link useCurrentFrame} asserts the frame exists - */ export function useMaybeCurrentFrame() { return useContext(CurrentFrameContext); } -/** - * Return the current frame, assert the frame exists. - * - * @see {@link useMaybeCurrentFrame} if you are not sure you are under a {@link StackedRouter}. - */ export function useCurrentFrame() { const frame = useMaybeCurrentFrame(); @@ -147,11 +130,8 @@ export function useCurrentFrame() { * A suspended frame is the one not on the top. "Suspended" * is the description of a certain situtation, not in the life cycle * of a frame. - * - * If this is not called under a {@link StackedRouter}, it always - * returns `false`. */ -export function useIsFrameSuspended() { +export function useMaybeIsFrameSuspended() { const { frames } = useMaybeNavigator() || {}; if (typeof frames === "undefined") { @@ -223,161 +203,10 @@ function serializableStack(stack: readonly StackFrame[]) { }); } -function isNotInIOSSwipeToBackArea(x: number) { - return ( - (x > 22 && x < window.innerWidth - 22) || - (x < -22 && x > window.innerWidth + 22) - ); -} - -function onEntryTouchStart(event: TouchEvent) { - if (event.touches.length !== 1) { - return; - } - - const [fig0] = event.touches; - - if (isNotInIOSSwipeToBackArea(fig0.clientX)) { - return; - } - - event.preventDefault(); -} - -/** - * This function contains the state for swipe to back. - * - * @returns the props for dialogs to feature swipe to back. - */ -function createManagedSwipeToBack( - stack: readonly Readonly[], - onlyPopFrame: (depth: number) => void, -) { - let reenterableAnimation: Animation | undefined; - let origWidth = 0, - origFigX = 0, - origFigY = 0; - - const resetAnimation = () => { - reenterableAnimation = undefined; - }; - - const onDialogTouchStart = ( - event: TouchEvent & { currentTarget: HTMLDialogElement }, - ) => { - if (event.touches.length !== 1) { - return; - } - event.stopPropagation(); - - const [fig0] = event.touches; - const { width } = event.currentTarget.getBoundingClientRect(); - origWidth = width; - origFigX = fig0.clientX; - origFigY = fig0.clientY; - - if (isNotInIOSSwipeToBackArea(fig0.clientX)) { - return; - } - // Prevent the default swipe to back/forward on iOS - - event.preventDefault(); - }; - - let animationProgressUpdateReleased = true; - let nextAnimationProgress = 0; - - const updateAnimationProgress = () => { - try { - if (!reenterableAnimation) return; - const { activeDuration, delay } = - reenterableAnimation.effect!.getComputedTiming(); - - const totalTime = (delay || 0) + Number(activeDuration); - reenterableAnimation.currentTime = totalTime * nextAnimationProgress; - } finally { - animationProgressUpdateReleased = true; - } - }; - - const onDialogTouchMove = ( - event: TouchEvent & { currentTarget: HTMLDialogElement }, - ) => { - if (event.touches.length !== 1) { - if (reenterableAnimation) { - reenterableAnimation.reverse(); - reenterableAnimation.play(); - } - } - - const [fig0] = event.touches; - - const ofsX = fig0.clientX - origFigX; - - if (!reenterableAnimation) { - if (!(ofsX > 22) || !(Math.abs(fig0.clientY - origFigY) < 44)) { - return; - } - const lastFr = stack[stack.length - 1]; - const createAnimation = lastFr.animateClose ?? animateClose; - reenterableAnimation = createAnimation(event.currentTarget); - reenterableAnimation.pause(); - reenterableAnimation.addEventListener("finish", resetAnimation); - reenterableAnimation.addEventListener("cancel", resetAnimation); - } - - event.preventDefault(); - event.stopPropagation(); - - nextAnimationProgress = ofsX / origWidth / window.devicePixelRatio; - - if (animationProgressUpdateReleased) { - animationProgressUpdateReleased = false; - - requestAnimationFrame(updateAnimationProgress); - } - }; - - const onDialogTouchEnd = (event: TouchEvent) => { - if (!reenterableAnimation) return; - - event.preventDefault(); - event.stopPropagation(); - - const { activeDuration, delay } = - reenterableAnimation.effect!.getComputedTiming(); - const totalTime = (delay || 0) + Number(activeDuration); - - if (Number(reenterableAnimation.currentTime) / totalTime > 0.1) { - reenterableAnimation.addEventListener("finish", () => { - onlyPopFrame(1); - }); - reenterableAnimation.play(); - } else { - reenterableAnimation.cancel(); - } - }; - - const onDialogTouchCancel = (event: TouchEvent) => { - if (!reenterableAnimation) return; - - event.preventDefault(); - event.stopPropagation(); - reenterableAnimation.cancel(); - }; - - return { - "on:touchstart": onDialogTouchStart, - "on:touchmove": onDialogTouchMove, - "on:touchend": onDialogTouchEnd, - "on:touchcancel": onDialogTouchCancel, - }; -} - /** * The router that stacks the pages. * - * **Routes** The router accepts the {@link RouterProps} excluding the "url" field. + * **Routes** The router accepts the {@link RouteProps} excluding the "url" field. * You can seamlessly use the `` from `@solidjs/router`. * * Be advised that this component is not a drop-in replacement of that router. @@ -411,7 +240,7 @@ function createManagedSwipeToBack( * Navigation animations (even the custom ones) will be played during * swipe to back, please keep in mind when designing animations. * - * The iOS default gesture is blocked on all pages. + * The iOS default gesture is blocked on those pages. */ const StackedRouter: Component = (oprops) => { const [stack, mutStack] = createStore([] as StackFrame[], { name: "stack" }); @@ -519,6 +348,7 @@ const StackedRouter: Component = (oprops) => { const oinsetRight = computedStyle .getPropertyValue("--safe-area-inset-right") .split("px", 1)[0]; + console.debug("insets-inline", oinsetLeft, oinsetRight); const left = Number(oinsetLeft), right = Number(oinsetRight.slice(0, oinsetRight.length - 2)); const totalWidth = SUBPAGE_MAX_WIDTH + left + right; @@ -535,7 +365,106 @@ const StackedRouter: Component = (oprops) => { }; }); - const swipeToBackProps = createManagedSwipeToBack(stack, onlyPopFrame); + let reenterableAnimation: Animation | undefined; + let origWidth = 0, + origFigX = 0, + origFigY = 0; + + const resetAnimation = () => { + reenterableAnimation = undefined; + }; + + const onDialogTouchStart = ( + event: TouchEvent & { currentTarget: HTMLDialogElement }, + ) => { + if (event.touches.length !== 1) { + return; + } + event.stopPropagation(); + + const [fig0] = event.touches; + const { width } = event.currentTarget.getBoundingClientRect(); + origWidth = width; + origFigX = fig0.clientX; + origFigY = fig0.clientY; + + const isNotInSwipeToBackArea = + (fig0.clientX > 22 && fig0.clientX < window.innerWidth - 22) || + (fig0.clientX < -22 && fig0.clientX > window.innerWidth + 22); + + if (isNotInSwipeToBackArea) { + return; + } + // Prevent the default swipe to back/forward on iOS + + event.preventDefault(); + }; + + const onDialogTouchMove = ( + event: TouchEvent & { currentTarget: HTMLDialogElement }, + ) => { + if (event.touches.length !== 1) { + if (reenterableAnimation) { + reenterableAnimation.reverse(); + reenterableAnimation.play(); + } + } + + const [fig0] = event.touches; + + const ofsX = fig0.clientX - origFigX; + + if (!reenterableAnimation) { + if (!(ofsX > 22) || !(Math.abs(fig0.clientY - origFigY) < 44)) { + return; + } + const lastFr = stack[stack.length - 1]; + const createAnimation = lastFr.animateClose ?? animateClose; + reenterableAnimation = createAnimation(event.currentTarget); + reenterableAnimation.pause(); + reenterableAnimation.addEventListener("finish", resetAnimation); + reenterableAnimation.addEventListener("cancel", resetAnimation); + } + + event.preventDefault(); + event.stopPropagation(); + + const pc = ofsX / origWidth / window.devicePixelRatio; + + const { activeDuration, delay } = + reenterableAnimation.effect!.getComputedTiming(); + + const totalTime = (delay || 0) + Number(activeDuration); + reenterableAnimation.currentTime = totalTime * pc; + }; + + const onDialogTouchEnd = (event: TouchEvent) => { + if (!reenterableAnimation) return; + + event.preventDefault(); + event.stopPropagation(); + + const { activeDuration, delay } = + reenterableAnimation.effect!.getComputedTiming(); + const totalTime = (delay || 0) + Number(activeDuration); + + if (Number(reenterableAnimation.currentTime) / totalTime > 0.1) { + reenterableAnimation.addEventListener("finish", () => { + onlyPopFrame(1); + }); + reenterableAnimation.play(); + } else { + reenterableAnimation.cancel(); + } + }; + + const onDialogTouchCancel = (event: TouchEvent) => { + if (!reenterableAnimation) return; + + event.preventDefault(); + event.stopPropagation(); + reenterableAnimation.cancel(); + }; return ( = (oprops) => { class="StackedPage" id={frame().rootId} role="presentation" - on:touchstart={onEntryTouchStart} > @@ -574,7 +502,10 @@ const StackedRouter: Component = (oprops) => { class="StackedPage" onCancel={[popFrame, 1]} onClick={[onDialogClick, popFrame]} - {...swipeToBackProps} + on:touchstart={onDialogTouchStart} + on:touchmove={onDialogTouchMove} + on:touchend={onDialogTouchEnd} + on:touchcancel={onDialogTouchCancel} id={frame().rootId} style={subInsets()} > diff --git a/src/timelines/MediaAttachmentGrid.css b/src/timelines/MediaAttachmentGrid.css index 34b22c9..880151a 100644 --- a/src/timelines/MediaAttachmentGrid.css +++ b/src/timelines/MediaAttachmentGrid.css @@ -1,7 +1,7 @@ .MediaAttachmentGrid { /* Note: MeidaAttachmentGrid has hard-coded layout calcalation */ margin-top: 1em; - margin-left: var(--card-pad, 0); + margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px); margin-right: var(--card-pad, 0); gap: 4px; contain: layout style; @@ -33,8 +33,4 @@ align-items: center; justify-content: center; } -} - -:where(.thread-top, .thread-mid) > .MediaAttachmentGrid { - margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px); } \ No newline at end of file diff --git a/src/timelines/MediaAttachmentGrid.tsx b/src/timelines/MediaAttachmentGrid.tsx index 65097b8..8fd14e3 100644 --- a/src/timelines/MediaAttachmentGrid.tsx +++ b/src/timelines/MediaAttachmentGrid.tsx @@ -207,7 +207,6 @@ const MediaAttachmentGrid: Component<{ style={style()} data-sort={index} data-media-type={item().type} - preload="metadata" /> @@ -223,7 +222,6 @@ const MediaAttachmentGrid: Component<{ style={style()} data-sort={index} data-media-type={item().type} - preload="metadata" /> diff --git a/src/timelines/PullDownToRefresh.tsx b/src/timelines/PullDownToRefresh.tsx index 5a61b46..7cbacdb 100644 --- a/src/timelines/PullDownToRefresh.tsx +++ b/src/timelines/PullDownToRefresh.tsx @@ -10,7 +10,7 @@ import { Refresh as RefreshIcon } from "@suid/icons-material"; import { CircularProgress } from "@suid/material"; import { makeEventListener } from "@solid-primitives/event-listener"; import { createVisibilityObserver } from "@solid-primitives/intersection-observer"; -import { useIsFrameSuspended } from "../platform/StackedRouter"; +import { useMaybeIsFrameSuspended } from "../platform/StackedRouter"; const PullDownToRefresh: Component<{ loading?: boolean; @@ -34,7 +34,7 @@ const PullDownToRefresh: Component<{ }); const rootVisible = obvx(() => rootElement); - const isFrameSuspended = useIsFrameSuspended() + const isFrameSuspended = useMaybeIsFrameSuspended() createEffect(() => { if (!rootVisible()) setPullDown(0); diff --git a/src/timelines/toots/TootContent.css b/src/timelines/toots/TootContent.css index 86ed977..79c4ffc 100644 --- a/src/timelines/toots/TootContent.css +++ b/src/timelines/toots/TootContent.css @@ -1,5 +1,6 @@ .TootContent { - margin-left: var(--card-pad, 0); + composes: cardNoPad from "../material/cards.module.css"; + margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px); margin-right: var(--card-pad, 0); line-height: 1.5; @@ -29,8 +30,4 @@ } } } -} - -:where(.thread-top, .thread-mid) > .TootContent { - margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px); } \ No newline at end of file