Files
mathew/frontend/app/utils/desktop.ts
Aran Roig e2c2c66d7f
Some checks failed
Build and Deploy Lean / build (push) Failing after 16m44s
Build and Deploy Nuxt / build (push) Failing after 48s
Vibecoded CI/CD
2026-10-06 00:23:17 +02:00

117 lines
4.8 KiB
TypeScript

/* Running as a desktop app.
One bundle answers both a browser and an app window (Tauri — see
src-tauri/), so nothing in `app/` is desktop code: it is the wiki asking at
run time whether it happens to be sitting in a window. The answer comes from
the shell, which marks the page it loads and marks no other — so the same
dist-desktop/ files opened in an ordinary browser behave as a web page, and
a web page that somehow met Tauri would only gain the shell's habits.
What a window does differently, then, is small and all about being a window
with one page and no browser chrome: a link leads out to the system's own
browser rather than replacing the wiki in the only view there is, and the
few bytes the app remembers of a person live where a window can be certain
of keeping them. */
import type { Ref } from 'vue'
import { watch } from 'vue'
/** The handle every Tauri webview injects into its page — present before the
* npm API is imported, and without it. Nothing else sets this name. */
export function isDesktop(): boolean {
if (!import.meta.client) return false
return (window as Window & { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ != null
}
/* A window is expected to have localStorage. A private browser may throw on
being asked for it at all, and a setting that cannot be remembered is worth
far less than a page that still draws — so the three doors below are the
only place any of it is touched, and none of them lets a throw through. */
export function storeGet(key: string): string | null {
try {
return localStorage.getItem(key)
} catch {
return null
}
}
export function storeSet(key: string, value: string): void {
try {
localStorage.setItem(key, value)
} catch {
/* nothing remembered; nothing else depends on it */
}
}
export function storeClear(key: string): void {
try {
localStorage.removeItem(key)
} catch {
/* as good as cleared, for as long as this page lives */
}
}
/** Whether an address leads outside this app — to a document on some other
* site, or to a mail client, or to a phone dialler. The app's own pages are
* the relative `/wiki/…` the router already knows; an absolute address that
* happens to name the wiki's public site counts as outward too, and opens
* there in the browser, which is the honest reading of a link written out in
* full. */
export function leadsOutward(href: string): boolean {
if (!import.meta.client || !href) return false
let url: URL
try {
url = new URL(href, window.location.href)
} catch {
return false
}
if (url.protocol === 'mailto:' || url.protocol === 'tel:') return true
return (url.protocol === 'http:' || url.protocol === 'https:') && url.origin !== window.location.origin
}
/** Hand a document to whatever machine-level application owns it: the browser
* for a page, the mail client for `mailto:`. In a browser a link already does
* this, so the fallback is the browser's own new tab; in a window there is no
* other tab, and following the link in place would leave the wiki's page
* gone from the only view there is. */
export async function openExternal(url: string): Promise<void> {
if (!import.meta.client || !url) return
if (isDesktop()) {
try {
const { openUrl } = await import('@tauri-apps/plugin-opener')
await openUrl(url)
return
} catch {
/* no shell to hand it to, or this window is not allowed this one — the
least worse thing left is the browser's way out */
}
}
window.open(url, '_blank', 'noopener')
}
/** Keep a session token where a window cannot lose it.
On the web a cookie is the whole of it: the server reads it, and the browser
keeps it as long as the API said the token lasts. A window's own address
(`tauri://localhost`) is one a webview is not everywhere obliged to keep
cookies for, and a session dying with the window would mean signing in again
every time the app was opened — for a token the API hands out for a week. So
here it is mirrored into localStorage and taken back from there whenever the
cookie comes up empty, whichever of the two this webview chose to keep.
The cookie still leads: it is the one already at hand, the one a web build
would have read, and the one the sign-out line clears. A browser asked this
question of the same files ignores all of it — there is nothing to mirror
where cookies work — so the wiki's behavior there is untouched. */
export function keepTokenRemembered(token: Ref<string | null>, key: string): void {
if (!import.meta.client || !isDesktop()) return
if (!token.value) {
const kept = storeGet(key)
if (kept) token.value = kept
}
// sync, so signing out clears the mirror in the same breath, not on a tick
watch(token, (value) => (value ? storeSet(key, value) : storeClear(key)), { flush: 'sync' })
}