feat(links): vælg hvilken browser et link åbner i

Ønsket var en liste over de installerede browsere. Den findes ikke: en
webside kan hverken se, hvilke browsere der er på maskinen — listen er
lukket land, fordi den er et fingeraftryk, der ville følge folk rundt på
nettet — eller bede systemet om at bruge en bestemt af dem.

Det eneste håndtag er de URL-skemaer, browserne selv registrerer:
firefox://open-url, googlechrome://, microsoft-edge:. Dem kan man prøve,
og man kan ikke spørge, om de findes. Så indstillingen er skrevet som
det, den er — et forsøg med et faldback — frem for som et løfte, der
ville holde på telefonen og ikke på en Mac.

Valget står i localStorage, ikke på kontoen. Samme grund som
sidebjælkens bredde: det er en egenskab ved maskinen, man sidder ved.
firefox:// virker på telefonen og gør ingenting på Mac'en, så et valg,
der fulgte kontoen, ville være forkert det ene af de to steder hver gang.

Ét sted i koden frem for ét pr. komponent. Links står i opgavetitler, i
beskrivelser, i noter og i indstillingerne, og en regel, der skulle
gentages hvert af de steder, ville mangle det ene sted, ingen tænkte på.
Kun udadgående links; programmets egne ruter går gennem SvelteKit som før.

To ting, målingen rettede undervejs:

Det skjulte iframe — den kendte opskrift — bliver blokeret af appens egen
Content-Security-Policy, som siger default-src 'self'. Uden e2e-prøven,
der fangede CSP-fejlen i konsollen, ville skemaet aldrig være blevet
forsøgt i produktion, og indstillingen ville have set ud til at virke,
fordi faldbacket åbnede linket alligevel. Nu sættes location i stedet:
et ukendt skema afbryder navigationen frem for at forlade siden.

Og faldbacket troede sig blokeret hver eneste gang. window.open med
noopener returnerer altid null — det står i specifikationen — så
kontrollen af, om fanen blev åbnet, faldt altid ud til "blokeret", og
den aktuelle fane blev navigeret væk i stedet. Håndtaget er det eneste,
der kan kende en blokeret fane fra en åbnet, så det bruges nu, og opener
ryddes bagefter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kristianwind
2026-09-09 08:41:21 +02:00
co-authored by Claude Opus 5
parent e931ddc313
commit 5eb689e4c2
6 changed files with 356 additions and 1 deletions
+34 -1
View File
@@ -11,6 +11,7 @@
import TaskDetail from '$lib/components/TaskDetail.svelte';
import { t } from '$lib/i18n.svelte.js';
import { setBadge } from '$lib/badge.js';
import { open as openInChosenBrowser, stored as storedOpener } from '$lib/linkopen.js';
import BeaconNotice from '$lib/components/BeaconNotice.svelte';
let { children } = $props();
@@ -55,6 +56,37 @@
app.load();
});
/**
* Links ud af programmet, åbnet i den browser, maskinen er sat op til.
*
* Ét sted frem for ét pr. komponent. Links står i opgavetitler, i
* beskrivelser, i noter og i indstillingerne, og en regel, der skulle
* gentages hvert af de steder, ville mangle det ene sted, ingen tænkte på.
*
* Kun udadgående. Programmets egne ruter går gennem SvelteKit og skal blive
* ved med at gøre det: en fane mere til ens egen notesside er ikke noget,
* nogen har bedt om.
*
* Er der ikke valgt nogen browser — hvilket er udgangspunktet — bliver
* klikket ikke rørt, og systemet gør, som det plejer.
*/
function onLinkClick(event) {
if (event.defaultPrevented || event.button !== 0) return;
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
if (!storedOpener().id) return;
const link = event.target?.closest?.('a[href]');
if (!link || link.target === '_self') return;
const href = link.getAttribute('href') ?? '';
if (!/^https?:\/\//i.test(href)) return;
// Vores egen adresse er ikke et link ud af programmet, uanset hvor den
// står — en fane mere til den side, man allerede er på, er ingen hjælp.
if (new URL(href, location.href).origin === location.origin) return;
event.preventDefault();
openInChosenBrowser(href);
}
// Tallet på appens ikon følger klokken. Ét sted, fordi der er ét tal: hentet,
// skubbet over websocket'en eller læst væk — mærket er det samme svar på alle
// tre, og en optælling pr. vej ville være tre, der kan blive uenige.
@@ -125,7 +157,8 @@
}
</script>
<svelte:window {onkeydown} />
<svelte:window
onclick={onLinkClick} {onkeydown} />
{#if app.loading}
<!-- Deliberately blank. A spinner for a request that resolves in 30ms is a