mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(articles): M7 share-target + bookmarklet — save from anywhere
@mana/shared-pwa gains PWAShareTarget + PWAShareTargetParams types
plus ManifestConfig.share_target pass-through. createPWAConfig now
accepts an optional `shareTarget` and threads it into the generated
manifest. Other apps keep working unchanged — the field is omitted
unless set.
Web app wiring:
- vite.config.ts passes shareTarget: { action: '/articles/add',
method: 'GET', params: { title, text, url } } so the installed PWA
shows up as a destination in the Android / Chromium share sheet.
- AddUrlForm reads ?url / ?text / ?title in onMount; falls back to
the first URL-shaped token in ?text because some senders (Chrome
Android, WhatsApp) put the shared link there instead of ?url. When
a URL is pre-filled the Readability preview auto-triggers, so the
user just hits "In Leseliste speichern" to confirm.
- New /articles/settings route hosts the bookmarklet (drag-to-
bookmarks-bar button + copy-to-clipboard + expandable snippet
viewer) and a short Share-Target explainer with an iOS-Safari
caveat. Linked from the ListView via a new gear button next to
"+ Neu speichern".
Bookmarklet form (origin-prefixed so it works across tenants):
javascript:void(window.open('${origin}/articles/add?url='+…))
Not in scope (plan marked optional): _pendingUrls offline queue.
Share without internet shows the existing error + retry state today;
can slot in as M7b if users hit it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
831c30eaa7
commit
8a991f7c39
8 changed files with 348 additions and 6 deletions
|
|
@ -79,9 +79,20 @@
|
||||||
<h1>Artikel</h1>
|
<h1>Artikel</h1>
|
||||||
<p class="subtitle">Später lesen — gespeicherte Web-Artikel, offline verfügbar.</p>
|
<p class="subtitle">Später lesen — gespeicherte Web-Artikel, offline verfügbar.</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="add-btn" onclick={() => goto('/articles/add')}>
|
<div class="header-actions">
|
||||||
+ Neu speichern
|
<button
|
||||||
</button>
|
type="button"
|
||||||
|
class="icon-btn"
|
||||||
|
title="Einstellungen — Bookmarklet + Share-Target"
|
||||||
|
aria-label="Artikel-Einstellungen"
|
||||||
|
onclick={() => goto('/articles/settings')}
|
||||||
|
>
|
||||||
|
⚙
|
||||||
|
</button>
|
||||||
|
<button type="button" class="add-btn" onclick={() => goto('/articles/add')}>
|
||||||
|
+ Neu speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-row" role="tablist" aria-label="Filter">
|
<div class="filter-row" role="tablist" aria-label="Filter">
|
||||||
|
|
@ -175,6 +186,26 @@
|
||||||
margin: 0 0 0.25rem 0;
|
margin: 0 0 0.25rem 0;
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
}
|
}
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.icon-btn {
|
||||||
|
padding: 0.5rem 0.65rem;
|
||||||
|
border-radius: 0.55rem;
|
||||||
|
border: 1px solid var(--color-border, rgba(0, 0, 0, 0.15));
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.icon-btn:hover {
|
||||||
|
border-color: var(--color-border-strong, rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
.add-btn {
|
.add-btn {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 0.55rem;
|
border-radius: 0.55rem;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
AddUrlForm — paste URL → preview → save.
|
AddUrlForm — paste URL → preview → save.
|
||||||
|
|
||||||
Flow:
|
Flow:
|
||||||
1. User pastes (or types) a URL
|
1. User pastes (or types) a URL, OR the page is opened with a URL
|
||||||
|
pre-filled via query string (?url=… / ?text=… / ?title=…). The
|
||||||
|
Web Share Target + bookmarklet both land here that way.
|
||||||
2. On "Vorschau abrufen": check scope-local dedupe first; if found,
|
2. On "Vorschau abrufen": check scope-local dedupe first; if found,
|
||||||
offer "öffnen" instead of re-extracting (saves one round-trip).
|
offer "öffnen" instead of re-extracting (saves one round-trip).
|
||||||
Otherwise call /api/v1/articles/extract and render the preview.
|
Otherwise call /api/v1/articles/extract and render the preview.
|
||||||
|
|
@ -10,9 +12,15 @@
|
||||||
articlesStore.saveFromExtracted — no second server call.
|
articlesStore.saveFromExtracted — no second server call.
|
||||||
4. Navigate into the new article so the user lands directly in the
|
4. Navigate into the new article so the user lands directly in the
|
||||||
reader view.
|
reader view.
|
||||||
|
|
||||||
|
Pre-filled URLs auto-trigger the preview on mount so the three-click
|
||||||
|
"share from browser → saved" flow really is three clicks: share →
|
||||||
|
pick Mana → hit "In Leseliste speichern".
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import { articlesStore } from '../stores/articles.svelte';
|
import { articlesStore } from '../stores/articles.svelte';
|
||||||
import { extractArticle, type ExtractedArticle } from '../api';
|
import { extractArticle, type ExtractedArticle } from '../api';
|
||||||
import type { Article } from '../types';
|
import type { Article } from '../types';
|
||||||
|
|
@ -31,6 +39,29 @@
|
||||||
node.focus();
|
node.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the first URL-shaped token from a string — some share
|
||||||
|
* senders (Chrome Android, WhatsApp) stuff the URL into the `text`
|
||||||
|
* slot instead of `url`, often prefixed with the page title.
|
||||||
|
*/
|
||||||
|
function firstUrl(text: string): string {
|
||||||
|
const m = text.match(/https?:\/\/\S+/i);
|
||||||
|
return m ? m[0] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const params = $page.url.searchParams;
|
||||||
|
const fromUrl = params.get('url')?.trim() ?? '';
|
||||||
|
const fromText = params.get('text')?.trim() ?? '';
|
||||||
|
const candidate = fromUrl || firstUrl(fromText);
|
||||||
|
if (candidate) {
|
||||||
|
url = candidate;
|
||||||
|
// Fire-and-forget — the handler is idempotent enough that a
|
||||||
|
// stray second click does no harm.
|
||||||
|
void handlePreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
preview = null;
|
preview = null;
|
||||||
duplicate = null;
|
duplicate = null;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
<!--
|
||||||
|
/articles/settings — collection of "how to save faster" tips.
|
||||||
|
|
||||||
|
Two surfaces today:
|
||||||
|
|
||||||
|
1. Bookmarklet — one-click save from any desktop browser. The user
|
||||||
|
drags the button into their bookmarks bar; clicking it opens
|
||||||
|
/articles/add?url=<current-page> in a new tab.
|
||||||
|
|
||||||
|
2. Share Target — installed PWA appears in the OS share sheet on
|
||||||
|
Android / Chromium desktop. Same landing route as the bookmarklet.
|
||||||
|
|
||||||
|
Both end up in AddUrlForm, which reads the ?url / ?text / ?title query
|
||||||
|
params, auto-triggers the Readability preview, and drops the user one
|
||||||
|
click away from "In Leseliste speichern".
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
// `origin` at render time — server-side rendering has no window, so
|
||||||
|
// we read it client-side after mount. The bookmarklet embeds the
|
||||||
|
// origin so the JavaScript URL works from any other origin's bookmark
|
||||||
|
// bar.
|
||||||
|
let origin = $state('');
|
||||||
|
onMount(() => {
|
||||||
|
origin = window.location.origin;
|
||||||
|
});
|
||||||
|
|
||||||
|
const bookmarklet = $derived(
|
||||||
|
origin
|
||||||
|
? `javascript:void(window.open('${origin}/articles/add?url='+encodeURIComponent(location.href)+'&title='+encodeURIComponent(document.title),'_blank'))`
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
|
||||||
|
let copyLabel = $state('Snippet kopieren');
|
||||||
|
|
||||||
|
async function copySnippet() {
|
||||||
|
if (!bookmarklet) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(bookmarklet);
|
||||||
|
copyLabel = 'Kopiert ✓';
|
||||||
|
setTimeout(() => (copyLabel = 'Snippet kopieren'), 1500);
|
||||||
|
} catch {
|
||||||
|
copyLabel = 'Fehler — bitte manuell kopieren';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Artikel-Einstellungen — Mana</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="settings-shell">
|
||||||
|
<header class="header">
|
||||||
|
<h1>Artikel-Einstellungen</h1>
|
||||||
|
<p class="subtitle">Schnellwege, um Artikel aus dem Browser in die Leseliste zu bekommen.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Bookmarklet</h2>
|
||||||
|
<p>
|
||||||
|
Zieh den Button unten in deine Lesezeichen-Leiste. Ein Klick auf einer beliebigen Webseite
|
||||||
|
öffnet
|
||||||
|
<code>/articles/add</code> mit der aktuellen URL vorausgefüllt — du bestätigst nur noch die Vorschau.
|
||||||
|
</p>
|
||||||
|
<div class="bookmarklet-row">
|
||||||
|
<!-- The anchor IS the bookmarklet: its href is the javascript: -->
|
||||||
|
<!-- snippet. We sanity-check it client-side (origin-prefixed and -->
|
||||||
|
<!-- opens a new tab) before persisting state, so there's no XSS -->
|
||||||
|
<!-- surface here beyond what the browser already allows on any -->
|
||||||
|
<!-- javascript: bookmark. -->
|
||||||
|
{#if bookmarklet}
|
||||||
|
<a class="bookmarklet" href={bookmarklet} onclick={(e) => e.preventDefault()}>
|
||||||
|
+ In Mana speichern
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<span class="muted">Bookmarklet wird geladen…</span>
|
||||||
|
{/if}
|
||||||
|
<button type="button" class="copy-btn" onclick={copySnippet} disabled={!bookmarklet}>
|
||||||
|
{copyLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<details class="snippet-details">
|
||||||
|
<summary>Quellcode anzeigen</summary>
|
||||||
|
<pre class="snippet">{bookmarklet}</pre>
|
||||||
|
</details>
|
||||||
|
<p class="hint">
|
||||||
|
Funktioniert in jedem Desktop-Browser. In Safari: Lesezeichen anlegen mit einer beliebigen
|
||||||
|
URL, dann nachträglich die URL durch das Snippet ersetzen.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Share-Target (Android / Chromium)</h2>
|
||||||
|
<p>
|
||||||
|
Wenn du Mana als App installierst (Browser-Menü „Zum Startbildschirm hinzufügen"), taucht
|
||||||
|
„Mana" in deinem OS-Share-Sheet auf. Teilen aus dem Browser oder einer anderen App → Mana
|
||||||
|
auswählen → Artikel wird direkt in der Leseliste vorgeschlagen.
|
||||||
|
</p>
|
||||||
|
<p class="hint">
|
||||||
|
iOS-Safari unterstützt die Web-Share-Target-API derzeit nicht — nutze dort das Bookmarklet.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.settings-shell {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
color: var(--color-text-muted, #64748b);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
padding: 1.1rem 1.2rem;
|
||||||
|
border: 1px solid var(--color-border, rgba(0, 0, 0, 0.1));
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: var(--color-surface, transparent);
|
||||||
|
}
|
||||||
|
.card h2 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
.card p {
|
||||||
|
margin: 0 0 0.75rem 0;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
color: var(--color-text-muted, #64748b);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
padding: 0.1rem 0.35rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
background: color-mix(in srgb, currentColor 8%, transparent);
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
.bookmarklet-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.bookmarklet {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.55rem;
|
||||||
|
background: #f97316;
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
user-select: none;
|
||||||
|
cursor: grab;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.bookmarklet:hover {
|
||||||
|
background: #ea580c;
|
||||||
|
}
|
||||||
|
.copy-btn {
|
||||||
|
padding: 0.5rem 0.85rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid var(--color-border, rgba(0, 0, 0, 0.15));
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.copy-btn:hover:not(:disabled) {
|
||||||
|
border-color: var(--color-border-strong, rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
.copy-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.muted {
|
||||||
|
color: var(--color-text-muted, #64748b);
|
||||||
|
}
|
||||||
|
.snippet-details {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.snippet-details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-muted, #64748b);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.snippet {
|
||||||
|
margin: 0.5rem 0 0 0;
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
background: color-mix(in srgb, currentColor 6%, transparent);
|
||||||
|
font-family: 'SF Mono', Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -49,6 +49,17 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
{ name: 'Chat', short_name: 'Chat', url: '/chat', description: 'Chat öffnen' },
|
{ name: 'Chat', short_name: 'Chat', url: '/chat', description: 'Chat öffnen' },
|
||||||
],
|
],
|
||||||
|
// Web Share Target — installed PWA shows up in the OS share
|
||||||
|
// sheet as "Mana" and lands on /articles/add with the URL
|
||||||
|
// pre-filled (AddUrlForm reads ?url + ?text + ?title). The
|
||||||
|
// `text` param is listed alongside `url` because some
|
||||||
|
// senders (Chrome Android, WhatsApp) stuff the URL into the
|
||||||
|
// text field.
|
||||||
|
shareTarget: {
|
||||||
|
action: '/articles/add',
|
||||||
|
method: 'GET',
|
||||||
|
params: { title: 'title', text: 'text', url: 'url' },
|
||||||
|
},
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,15 @@
|
||||||
|
|
||||||
**M5 Migration von news:type='saved': DONE** (commit `04293ed5e`) — Boot-gated Migration in `modules/articles/migrations/from-news.ts` (localStorage-Sentinel `mana:articles:from-news-migration:v1`), decrypt→re-encrypt zwischen den beiden Field-Allowlists, Status-Mapping `isArchived→archived` / `isRead→finished` / sonst `unread`, Source-Rows werden soft-deletet. News-Code deprecated: `saveFromUrl` + `extractFromUrl` entfernt, `save_news_article` AI-Tool behält seinen Namen (wegen Mission-History) und leitet intern aufs `articles`-Modul um. `/news/add` + `/news/saved` sind Redirects. `news-research` „Speichern"-Buttons routen auf `/articles/[id]`.
|
**M5 Migration von news:type='saved': DONE** (commit `04293ed5e`) — Boot-gated Migration in `modules/articles/migrations/from-news.ts` (localStorage-Sentinel `mana:articles:from-news-migration:v1`), decrypt→re-encrypt zwischen den beiden Field-Allowlists, Status-Mapping `isArchived→archived` / `isRead→finished` / sonst `unread`, Source-Rows werden soft-deletet. News-Code deprecated: `saveFromUrl` + `extractFromUrl` entfernt, `save_news_article` AI-Tool behält seinen Namen (wegen Mission-History) und leitet intern aufs `articles`-Modul um. `/news/add` + `/news/saved` sind Redirects. `news-research` „Speichern"-Buttons routen auf `/articles/[id]`.
|
||||||
|
|
||||||
**M6 AI-Tools: DONE** (commit pending) — 5 neue Einträge im `AI_TOOL_CATALOG` (`shared-ai/src/tools/schemas.ts`): `list_articles` (auto), `save_article` / `archive_article` / `tag_article` / `add_article_highlight` (alle propose). `modules/articles/tools.ts` enthält die `execute`-Funktionen, registriert in `data/tools/init.ts`. `tag_article` dedupliziert case-insensitive über den globalen Pool und legt Tags via `tagMutations.createTag` an falls nötig. `add_article_highlight` snappt auf die erste wörtliche Fundstelle in `article.content` und lehnt den Call ab wenn der Text nicht exakt vorkommt (kein Orphan-Highlight). Policy/Executor/Server-Planner leiten sich automatisch aus dem Katalog ab.
|
**M6 AI-Tools: DONE** (commit `5924f4fac`) — 5 neue Einträge im `AI_TOOL_CATALOG` (`shared-ai/src/tools/schemas.ts`): `list_articles` (auto), `save_article` / `archive_article` / `tag_article` / `add_article_highlight` (alle propose). `modules/articles/tools.ts` enthält die `execute`-Funktionen, registriert in `data/tools/init.ts`. `tag_article` dedupliziert case-insensitive über den globalen Pool und legt Tags via `tagMutations.createTag` an falls nötig. `add_article_highlight` snappt auf die erste wörtliche Fundstelle in `article.content` und lehnt den Call ab wenn der Text nicht exakt vorkommt (kein Orphan-Highlight). Policy/Executor/Server-Planner leiten sich automatisch aus dem Katalog ab.
|
||||||
|
|
||||||
**Hinweis AiProposalInbox:** Der apps/mana/CLAUDE.md-Abschnitt erwähnt `<AiProposalInbox module="articles" />` als Inline-Mount, aber die Komponente existiert im aktuellen Codebase nicht — nach dem `pendingProposals`-Table-Drop in Dexie v29 wurde die Proposal-Darstellung auf `server-iteration-staging` + den Cross-Module-Inbox im Mission-Detail-View umgestellt. Articles-Proposals tauchen dort automatisch auf. Falls die Inline-Komponente wieder reaktiviert wird, muss nur der Mount in `ListView.svelte` ergänzt werden.
|
**Hinweis AiProposalInbox:** Der apps/mana/CLAUDE.md-Abschnitt erwähnt `<AiProposalInbox module="articles" />` als Inline-Mount, aber die Komponente existiert im aktuellen Codebase nicht — nach dem `pendingProposals`-Table-Drop in Dexie v29 wurde die Proposal-Darstellung auf `server-iteration-staging` + den Cross-Module-Inbox im Mission-Detail-View umgestellt. Articles-Proposals tauchen dort automatisch auf. Falls die Inline-Komponente wieder reaktiviert wird, muss nur der Mount in `ListView.svelte` ergänzt werden.
|
||||||
|
|
||||||
Nächster Schritt: M7 (Share-Target + Bookmarklet) oder M8 (HighlightsView + Stats + Dashboard-Widget).
|
**M7 Share-Target + Bookmarklet: DONE** (commit pending) — `@mana/shared-pwa` bekommt neue Types (`PWAShareTarget`, `PWAShareTargetParams`), `createPWAConfig` threadet `shareTarget` in den Manifest, `ManifestConfig.share_target?` ergänzt. Web-App: `vite.config.ts` setzt `shareTarget: { action: '/articles/add', method: 'GET', params: { title, text, url } }`; `AddUrlForm` liest Query-Params in `onMount` (inkl. URL-Regex-Fallback auf `text` weil Chrome Android / WhatsApp den Link dort reinstecken), triggert auto-Vorschau. Neue Route `/articles/settings` rendert Bookmarklet-Karte (Drag-to-Bookmark + Copy-Snippet + expandable Quellcode) und Share-Target-Erklärung. `ListView` bekommt Zahnrad-Button zum Settings-Aufruf.
|
||||||
|
|
||||||
|
Nicht im Scope (bewusst ausgelassen): die „optional" im Plan markierte `_pendingUrls`-Offline-Queue. Kann als M7b nachgereicht werden wenn das Problem auftaucht.
|
||||||
|
|
||||||
|
Nächster Schritt: M8 (HighlightsView + Stats + Dashboard-Widget).
|
||||||
|
|
||||||
## Ziel
|
## Ziel
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ export function createPWAConfig(options: PWAConfigOptions): PWAConfig {
|
||||||
backgroundColor = DEFAULT_BACKGROUND_COLOR,
|
backgroundColor = DEFAULT_BACKGROUND_COLOR,
|
||||||
preset = 'standard',
|
preset = 'standard',
|
||||||
shortcuts = [],
|
shortcuts = [],
|
||||||
|
shareTarget,
|
||||||
categories = DEFAULT_CATEGORIES,
|
categories = DEFAULT_CATEGORIES,
|
||||||
includeAssets = [],
|
includeAssets = [],
|
||||||
globIgnores = [],
|
globIgnores = [],
|
||||||
|
|
@ -91,6 +92,17 @@ export function createPWAConfig(options: PWAConfigOptions): PWAConfig {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Web Share Target — lets installed PWAs appear in the OS share
|
||||||
|
// sheet. Browsers that don't support the spec ignore the field.
|
||||||
|
if (shareTarget) {
|
||||||
|
manifest.share_target = {
|
||||||
|
action: shareTarget.action,
|
||||||
|
method: shareTarget.method ?? 'GET',
|
||||||
|
enctype: shareTarget.enctype,
|
||||||
|
params: shareTarget.params,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Build workbox config
|
// Build workbox config
|
||||||
const workbox: WorkboxConfig = {
|
const workbox: WorkboxConfig = {
|
||||||
globPatterns: DEFAULT_GLOB_PATTERNS,
|
globPatterns: DEFAULT_GLOB_PATTERNS,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ export type {
|
||||||
PWAConfigOptions,
|
PWAConfigOptions,
|
||||||
PWAConfig,
|
PWAConfig,
|
||||||
PWAShortcut,
|
PWAShortcut,
|
||||||
|
PWAShareTarget,
|
||||||
|
PWAShareTargetParams,
|
||||||
WorkboxPreset,
|
WorkboxPreset,
|
||||||
ManifestConfig,
|
ManifestConfig,
|
||||||
ManifestIcon,
|
ManifestIcon,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,36 @@ export interface PWAShortcut {
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web Share Target API configuration. When an installed PWA declares
|
||||||
|
* a share target, the OS share sheet offers the app as a destination
|
||||||
|
* for URLs / text / titles. The browser invokes `action` with the
|
||||||
|
* selected data mapped to the `params` field names.
|
||||||
|
*
|
||||||
|
* Reference: https://www.w3.org/TR/web-share-target/
|
||||||
|
* Browser support: Chromium (Android + desktop installed PWAs).
|
||||||
|
* Safari / Firefox ignore the field gracefully.
|
||||||
|
*/
|
||||||
|
export interface PWAShareTargetParams {
|
||||||
|
/** Query/form param that carries the shared page title. */
|
||||||
|
title?: string;
|
||||||
|
/** Query/form param for free-form shared text. */
|
||||||
|
text?: string;
|
||||||
|
/** Query/form param for the shared URL. */
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PWAShareTarget {
|
||||||
|
/** In-app URL the browser should navigate to with the shared payload. */
|
||||||
|
action: string;
|
||||||
|
/** HTTP method. GET maps data to query params, POST to form data. */
|
||||||
|
method?: 'GET' | 'POST';
|
||||||
|
/** Required for method=POST — typical value: 'multipart/form-data'. */
|
||||||
|
enctype?: string;
|
||||||
|
/** Maps the spec's title/text/url slots onto your own param names. */
|
||||||
|
params: PWAShareTargetParams;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration options for createPWAConfig
|
* Configuration options for createPWAConfig
|
||||||
*/
|
*/
|
||||||
|
|
@ -67,6 +97,13 @@ export interface PWAConfigOptions {
|
||||||
*/
|
*/
|
||||||
shortcuts?: PWAShortcut[];
|
shortcuts?: PWAShortcut[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web Share Target config. Installed PWA becomes a destination in
|
||||||
|
* the OS share sheet for URLs / text / titles. Ignored by browsers
|
||||||
|
* that don't support the spec (Safari / Firefox).
|
||||||
|
*/
|
||||||
|
shareTarget?: PWAShareTarget;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App categories for store listings
|
* App categories for store listings
|
||||||
* @default ["productivity", "utilities"]
|
* @default ["productivity", "utilities"]
|
||||||
|
|
@ -165,6 +202,12 @@ export interface ManifestConfig {
|
||||||
url: string;
|
url: string;
|
||||||
icons?: Array<{ src: string; sizes: string }>;
|
icons?: Array<{ src: string; sizes: string }>;
|
||||||
}>;
|
}>;
|
||||||
|
share_target?: {
|
||||||
|
action: string;
|
||||||
|
method: 'GET' | 'POST';
|
||||||
|
enctype?: string;
|
||||||
|
params: PWAShareTargetParams;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue