Compare commits

..

No commits in common. "57b242c93f24e93aa3d215e5894c0900db8af9e0" and "6726ffe664a39bc04391c3e3d563b2e7617ce2da" have entirely different histories.

5 changed files with 113 additions and 191 deletions

View file

@ -79,11 +79,6 @@ export type Navigator<PushGuide = Record<string, any>> = {
const NavigatorContext = /* @__PURE__ */ createContext<Navigator>(); const NavigatorContext = /* @__PURE__ */ createContext<Navigator>();
/**
* Get the possible navigator of the {@link StackedRouter}.
*
* @see useNavigator for the navigator usage.
*/
export function useMaybeNavigator() { export function useMaybeNavigator() {
return useContext(NavigatorContext); return useContext(NavigatorContext);
} }
@ -96,8 +91,6 @@ export function useMaybeNavigator() {
* path and its state. If you need push guide, you may want to * path and its state. If you need push guide, you may want to
* define your own function (like `useAppNavigator`) and cast the * define your own function (like `useAppNavigator`) and cast the
* navigator to the type you need. * navigator to the type you need.
*
* @see {@link useMaybeNavigator} if you are not sure you are under a {@link StackedRouter}.
*/ */
export function useNavigator() { export function useNavigator() {
const navigator = useMaybeNavigator(); const navigator = useMaybeNavigator();
@ -117,20 +110,10 @@ export type CurrentFrame = {
const CurrentFrameContext = const CurrentFrameContext =
/* @__PURE__ */ createContext<Accessor<Readonly<CurrentFrame>>>(); /* @__PURE__ */ createContext<Accessor<Readonly<CurrentFrame>>>();
/**
* Return the current, if possible.
*
* @see {@link useCurrentFrame} asserts the frame exists
*/
export function useMaybeCurrentFrame() { export function useMaybeCurrentFrame() {
return useContext(CurrentFrameContext); 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() { export function useCurrentFrame() {
const frame = useMaybeCurrentFrame(); const frame = useMaybeCurrentFrame();
@ -147,11 +130,8 @@ export function useCurrentFrame() {
* A suspended frame is the one not on the top. "Suspended" * A suspended frame is the one not on the top. "Suspended"
* is the description of a certain situtation, not in the life cycle * is the description of a certain situtation, not in the life cycle
* of a frame. * 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() || {}; const { frames } = useMaybeNavigator() || {};
if (typeof frames === "undefined") { 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<StackFrame>[],
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. * 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 `<Route />` from `@solidjs/router`. * You can seamlessly use the `<Route />` from `@solidjs/router`.
* *
* Be advised that this component is not a drop-in replacement of that 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 * Navigation animations (even the custom ones) will be played during
* swipe to back, please keep in mind when designing animations. * 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<StackedRouterProps> = (oprops) => { const StackedRouter: Component<StackedRouterProps> = (oprops) => {
const [stack, mutStack] = createStore([] as StackFrame[], { name: "stack" }); const [stack, mutStack] = createStore([] as StackFrame[], { name: "stack" });
@ -519,6 +348,7 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
const oinsetRight = computedStyle const oinsetRight = computedStyle
.getPropertyValue("--safe-area-inset-right") .getPropertyValue("--safe-area-inset-right")
.split("px", 1)[0]; .split("px", 1)[0];
console.debug("insets-inline", oinsetLeft, oinsetRight);
const left = Number(oinsetLeft), const left = Number(oinsetLeft),
right = Number(oinsetRight.slice(0, oinsetRight.length - 2)); right = Number(oinsetRight.slice(0, oinsetRight.length - 2));
const totalWidth = SUBPAGE_MAX_WIDTH + left + right; const totalWidth = SUBPAGE_MAX_WIDTH + left + right;
@ -535,7 +365,106 @@ const StackedRouter: Component<StackedRouterProps> = (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 ( return (
<NavigatorContext.Provider <NavigatorContext.Provider
@ -563,7 +492,6 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
class="StackedPage" class="StackedPage"
id={frame().rootId} id={frame().rootId}
role="presentation" role="presentation"
on:touchstart={onEntryTouchStart}
> >
<StaticRouter url={frame().path} {...oprops} /> <StaticRouter url={frame().path} {...oprops} />
</div> </div>
@ -574,7 +502,10 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
class="StackedPage" class="StackedPage"
onCancel={[popFrame, 1]} onCancel={[popFrame, 1]}
onClick={[onDialogClick, popFrame]} onClick={[onDialogClick, popFrame]}
{...swipeToBackProps} on:touchstart={onDialogTouchStart}
on:touchmove={onDialogTouchMove}
on:touchend={onDialogTouchEnd}
on:touchcancel={onDialogTouchCancel}
id={frame().rootId} id={frame().rootId}
style={subInsets()} style={subInsets()}
> >

View file

@ -1,7 +1,7 @@
.MediaAttachmentGrid { .MediaAttachmentGrid {
/* Note: MeidaAttachmentGrid has hard-coded layout calcalation */ /* Note: MeidaAttachmentGrid has hard-coded layout calcalation */
margin-top: 1em; 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); margin-right: var(--card-pad, 0);
gap: 4px; gap: 4px;
contain: layout style; contain: layout style;
@ -33,8 +33,4 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
}
:where(.thread-top, .thread-mid) > .MediaAttachmentGrid {
margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px);
} }

View file

@ -207,7 +207,6 @@ const MediaAttachmentGrid: Component<{
style={style()} style={style()}
data-sort={index} data-sort={index}
data-media-type={item().type} data-media-type={item().type}
preload="metadata"
/> />
</Match> </Match>
<Match when={itemType() === "gifv"}> <Match when={itemType() === "gifv"}>
@ -223,7 +222,6 @@ const MediaAttachmentGrid: Component<{
style={style()} style={style()}
data-sort={index} data-sort={index}
data-media-type={item().type} data-media-type={item().type}
preload="metadata"
/> />
</Match> </Match>
<Match when={itemType() === "audio"}> <Match when={itemType() === "audio"}>

View file

@ -10,7 +10,7 @@ import { Refresh as RefreshIcon } from "@suid/icons-material";
import { CircularProgress } from "@suid/material"; import { CircularProgress } from "@suid/material";
import { makeEventListener } from "@solid-primitives/event-listener"; import { makeEventListener } from "@solid-primitives/event-listener";
import { createVisibilityObserver } from "@solid-primitives/intersection-observer"; import { createVisibilityObserver } from "@solid-primitives/intersection-observer";
import { useIsFrameSuspended } from "../platform/StackedRouter"; import { useMaybeIsFrameSuspended } from "../platform/StackedRouter";
const PullDownToRefresh: Component<{ const PullDownToRefresh: Component<{
loading?: boolean; loading?: boolean;
@ -34,7 +34,7 @@ const PullDownToRefresh: Component<{
}); });
const rootVisible = obvx(() => rootElement); const rootVisible = obvx(() => rootElement);
const isFrameSuspended = useIsFrameSuspended() const isFrameSuspended = useMaybeIsFrameSuspended()
createEffect(() => { createEffect(() => {
if (!rootVisible()) setPullDown(0); if (!rootVisible()) setPullDown(0);

View file

@ -1,5 +1,6 @@
.TootContent { .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); margin-right: var(--card-pad, 0);
line-height: 1.5; 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);
} }