mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
An installed app had no title bar of its own, no way to save attachments except a zip, and "pop out" stayed inside the page. A new window was a second copy of the app that had to restore the session itself, and on start-up that evicts any login made without "Remember me" from the list every window shares. - Title bar: `display_override` with `window-controls-overlay`, one `--titlebar-inset` variable at the page roots and skeletons, and a drag strip with the window title. 0 without an overlay, so nothing moves. - Save all to a folder: `showDirectoryPicker`, asking the device policy before the picker and before each file, with the zip as the fallback. - Pop-out routes `/popout/message/<id>` and `/popout/compose`, on offer in an installed app or when the desktop bridge has windows. Hidden in Lite. - A pop-out borrows the main window's session and never evicts or starts a token timer. Every renewal now takes a Web Lock, so two windows can't send the same refresh token, which Stalwart 1.0 may treat as a replay. - A draft moves to its window under the same id. A closing window hands unsaved text to the main window to save, since its own request wouldn't survive the close. - Read state, drafts, theme and language follow across windows, and `html[data-desktop]` gates the app-like behaviour in `app/desktop.css`.
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
/**
|
|
* Window Controls Overlay: an installed app (and the desktop app's
|
|
* titleBarOverlay window) draws the page up to the top of the window and lays
|
|
* the native minimise, maximise and close buttons over it.
|
|
*
|
|
* Asks navigator.windowControlsOverlay, never the media query:
|
|
* `(display-mode: window-controls-overlay)` is false in Electron with
|
|
* titleBarOverlay, while the API and env(titlebar-area-*) work there.
|
|
*
|
|
* app/globals.css gives --titlebar-inset its first value from
|
|
* env(titlebar-area-height, 0px), so the page has the right top padding from
|
|
* the first paint, before any script runs. This module keeps the variable in
|
|
* step with the API afterwards, which is what a browser or bridge without the
|
|
* env() values relies on.
|
|
*/
|
|
|
|
export interface TitlebarArea {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
interface OverlayLike extends EventTarget {
|
|
visible: boolean;
|
|
getTitlebarAreaRect?: () => { x: number; y: number; width: number; height: number };
|
|
}
|
|
|
|
export const TITLEBAR_INSET_VAR = '--titlebar-inset';
|
|
|
|
function overlay(): OverlayLike | null {
|
|
if (typeof navigator === 'undefined') return null;
|
|
const value = (navigator as Navigator & { windowControlsOverlay?: OverlayLike }).windowControlsOverlay;
|
|
return value ?? null;
|
|
}
|
|
|
|
/** The strip the page may draw in next to the window's controls, or null when there is no overlay. */
|
|
export function readTitlebarArea(): TitlebarArea | null {
|
|
const current = overlay();
|
|
if (!current?.visible) return null;
|
|
const rect = current.getTitlebarAreaRect?.();
|
|
if (!rect || !(rect.height > 0)) return null;
|
|
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
}
|
|
|
|
/**
|
|
* Sets --titlebar-inset for this area. With no area the inline value goes, so
|
|
* the stylesheet's value (0 without an overlay) is what counts again.
|
|
*/
|
|
export function applyTitlebarInset(area: TitlebarArea | null, root: HTMLElement = document.documentElement): void {
|
|
if (area) root.style.setProperty(TITLEBAR_INSET_VAR, `${area.height}px`);
|
|
else root.style.removeProperty(TITLEBAR_INSET_VAR);
|
|
}
|
|
|
|
/**
|
|
* Calls `listener` now and whenever the overlay's size or visibility changes
|
|
* (`geometrychange`). Without the API it calls it once with null and listens
|
|
* to nothing, so a browser without it runs no extra code. Returns the
|
|
* unsubscribe.
|
|
*/
|
|
export function watchTitlebarArea(listener: (area: TitlebarArea | null) => void): () => void {
|
|
listener(readTitlebarArea());
|
|
const current = overlay();
|
|
if (!current) return () => {};
|
|
const onChange = () => listener(readTitlebarArea());
|
|
current.addEventListener('geometrychange', onChange);
|
|
return () => current.removeEventListener('geometrychange', onChange);
|
|
}
|