desktop: keep drag/resize working over embedded iframes + add embed loading spinner
This commit is contained in:
parent
e740a0061b
commit
bc738541cf
5 changed files with 65 additions and 3 deletions
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { type CSSProperties, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import type { AppDef } from "./apps";
|
||||
|
||||
export interface WinState {
|
||||
|
|
@ -49,6 +49,10 @@ export default function WinWindow({
|
|||
onMove,
|
||||
onResize,
|
||||
}: Props) {
|
||||
// While a drag/resize is in flight we disarm the embedded iframe (see the
|
||||
// `.interacting` rule in skeuo.css). Otherwise a service window swallows the
|
||||
// pointer as soon as it passes over the site and the gesture freezes.
|
||||
const [interacting, setInteracting] = useState(false);
|
||||
const centered = win.centered && !win.maximized;
|
||||
const style: CSSProperties = win.maximized
|
||||
? { left: 0, top: 0, width: "100%", height: "100%", zIndex: win.z }
|
||||
|
|
@ -62,6 +66,7 @@ export default function WinWindow({
|
|||
if ((e.target as HTMLElement).closest(".win-controls")) return;
|
||||
|
||||
e.preventDefault();
|
||||
setInteracting(true);
|
||||
let ox = win.x;
|
||||
let oy = win.y;
|
||||
if (win.centered) {
|
||||
|
|
@ -79,6 +84,7 @@ export default function WinWindow({
|
|||
onMove(ox + (ev.clientX - sx), oy + (ev.clientY - sy));
|
||||
};
|
||||
const handleUp = () => {
|
||||
setInteracting(false);
|
||||
window.removeEventListener("pointermove", handleMove);
|
||||
window.removeEventListener("pointerup", handleUp);
|
||||
window.removeEventListener("pointercancel", handleUp);
|
||||
|
|
@ -95,6 +101,7 @@ export default function WinWindow({
|
|||
if (win.maximized) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setInteracting(true);
|
||||
|
||||
const node = e.currentTarget.closest(".win-window") as HTMLElement | null;
|
||||
const rect = node?.getBoundingClientRect();
|
||||
|
|
@ -137,6 +144,7 @@ export default function WinWindow({
|
|||
onResize(Math.round(x), Math.round(y), Math.round(w), Math.round(h));
|
||||
};
|
||||
const handleUp = () => {
|
||||
setInteracting(false);
|
||||
window.removeEventListener("pointermove", handleMove);
|
||||
window.removeEventListener("pointerup", handleUp);
|
||||
window.removeEventListener("pointercancel", handleUp);
|
||||
|
|
@ -156,7 +164,8 @@ export default function WinWindow({
|
|||
(win.minimized ? " minimized" : "") +
|
||||
(win.maximized ? " maximized" : "") +
|
||||
(closing ? " closing" : "") +
|
||||
(minimizing ? " minimizing" : "")
|
||||
(minimizing ? " minimizing" : "") +
|
||||
(interacting ? " interacting" : "")
|
||||
}
|
||||
style={style}
|
||||
onPointerDown={onFocus}
|
||||
|
|
|
|||
|
|
@ -818,12 +818,20 @@ function WebsiteApp({
|
|||
|
||||
/** Renders a site in an iframe. Only usable for sites that allow framing. */
|
||||
function EmbeddedSite({ title, url }: { title: string; url: string }) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
return (
|
||||
<div className="website-embed">
|
||||
{!loaded && (
|
||||
<div className="website-embed-loading" role="status">
|
||||
<span className="website-embed-spinner" aria-hidden="true" />
|
||||
<span>Loading {title}…</span>
|
||||
</div>
|
||||
)}
|
||||
<iframe
|
||||
className="website-embed-frame"
|
||||
src={url}
|
||||
title={title}
|
||||
onLoad={() => setLoaded(true)}
|
||||
allow="autoplay; fullscreen; encrypted-media; clipboard-read; clipboard-write"
|
||||
allowFullScreen
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -686,6 +686,7 @@ a.win-icon {
|
|||
/* Live service sites rendered inside the window. The site fills the window
|
||||
body edge-to-edge, so the iframe owns all the scrolling. */
|
||||
.website-embed {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
|
|
@ -698,6 +699,39 @@ a.win-icon {
|
|||
border: 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
/* While the window is dragged/resized, disown the iframe's pointer so the
|
||||
gesture isn't swallowed when the cursor passes over the embedded site. */
|
||||
.win-window.interacting .website-embed-frame {
|
||||
pointer-events: none;
|
||||
}
|
||||
/* Navidrome/Forgejo are full SPAs, so the frame is blank for a moment while
|
||||
they boot — cover it with a spinner until the iframe reports load. */
|
||||
.website-embed-loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
background: #232629;
|
||||
color: #9fd0f5;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.website-embed-spinner {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid rgba(159, 208, 245, 0.25);
|
||||
border-top-color: #3daee9;
|
||||
animation: website-embed-spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes website-embed-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
MINECRAFT SMP JOIN MENU (opened from the SMP service shortcut)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue