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:
co-authored by
Claude Opus 5
parent
e931ddc313
commit
5eb689e4c2
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user