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
+63
View File
@@ -1,6 +1,12 @@
<script>
/** Konto: who you are, your password, and the second factor. */
import { api, humanMessage } from '$lib/api.js';
import {
OPENERS,
stored as storedOpener,
save as saveOpenerChoice,
open as openLink
} from '$lib/linkopen.js';
import { app, theme, THEMES, look, LOOKS, menuSize, textSize, SIZES } from '$lib/stores.svelte.js';
import { t, plural, i18n } from '$lib/i18n.svelte.js';
import { supported, register } from '$lib/passkey.js';
@@ -8,6 +14,20 @@
// --- profile ---------------------------------------------------------------
// Hvilken browser links åbner i. På apparatet, ikke på kontoen: `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.
let opener = $state(storedOpener());
function saveOpener(choice) {
opener = choice;
saveOpenerChoice(choice);
}
function chooseOpener(id) {
saveOpener({ id, template: id === 'custom' ? opener.template : '' });
}
let name = $state('');
let timezone = $state('');
let locale = $state('da');
@@ -401,6 +421,49 @@
</div>
</section>
<section class="panel">
<header>
<h2>{t('link.title')}</h2>
<p class="hint">{t('link.hint')}</p>
</header>
<div class="field">
<label for="opener">{t('link.opener')}</label>
<select id="opener" value={opener.id} onchange={(e) => chooseOpener(e.currentTarget.value)}>
{#each OPENERS as option (option.id)}
<option value={option.id}>{t(option.label)}</option>
{/each}
<option value="custom">{t('link.openerCustom')}</option>
</select>
</div>
{#if opener.id === 'custom'}
<div class="field">
<label for="openertpl">{t('link.template')}</label>
<input
id="openertpl"
class="mono"
value={opener.template}
placeholder="firefox://open-url?url={'{encoded}'}"
onchange={(e) => saveOpener({ id: 'custom', template: e.currentTarget.value })}
/>
<p class="hint">{t('link.templateHint')}</p>
</div>
{/if}
{#if opener.id}
<!-- Et forsøg frem for et løfte. Der er ingen måde at spørge, om et skema
findes — man kan kun prøve, og så er det bedre at prøve på en adresse,
man selv har valgt, end at opdage det på et rigtigt link. -->
<div class="row">
<button class="secondary" onclick={() => openLink('https://example.dk')}>
{t('link.try')}
</button>
<span class="hint">{t('link.tryHint')}</span>
</div>
{/if}
</section>
<section class="panel">
<header>
<h2>{t('account.password')}</h2>