feat(post): Vorlesen-Button (Web Speech API), nur wenn unterstützt
PostActions: 🔊 Vorlesen liest Artikeltext (Titel+Body, versteckte Elemente gefiltert), Toggle Pause/Weiter + Stop, deutsche Stimme, satzweise, stoppt bei Seitenwechsel. Button erscheint nur wenn speechSynthesis verfügbar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from "@iconify/svelte";
|
import Icon from "@iconify/svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import "$lib/iconify-offline";
|
import "$lib/iconify-offline";
|
||||||
import { env } from "$env/dynamic/public";
|
import { env } from "$env/dynamic/public";
|
||||||
import { useTranslate, T } from "$lib/translations";
|
import { useTranslate, T } from "$lib/translations";
|
||||||
@@ -156,6 +156,73 @@
|
|||||||
function print() {
|
function print() {
|
||||||
window.print();
|
window.print();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Vorlesen (Web Speech API) ───────────────────────────────────────────
|
||||||
|
// Button erscheint nur, wenn der Browser Sprachausgabe unterstützt.
|
||||||
|
let canSpeak = $state(false);
|
||||||
|
let speaking = $state(false);
|
||||||
|
let paused = $state(false);
|
||||||
|
$effect(() => {
|
||||||
|
canSpeak = typeof window !== "undefined" && "speechSynthesis" in window;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getArticleText(): string {
|
||||||
|
const art = document.querySelector("article");
|
||||||
|
if (!art) return "";
|
||||||
|
const clone = art.cloneNode(true) as HTMLElement;
|
||||||
|
clone
|
||||||
|
.querySelectorAll("[hidden], .sr-only, script, style, nav, figure figcaption")
|
||||||
|
.forEach((n) => n.remove());
|
||||||
|
return (clone.textContent ?? "").replace(/\s+/g, " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function speak() {
|
||||||
|
const synth = window.speechSynthesis;
|
||||||
|
synth.cancel();
|
||||||
|
const text = getArticleText();
|
||||||
|
if (!text) return;
|
||||||
|
// In Sätze zerlegen — manche Engines schneiden lange Utterances ab.
|
||||||
|
const chunks = text.match(/[^.!?]+[.!?]*(?:\s|$)/g) ?? [text];
|
||||||
|
const voice =
|
||||||
|
synth.getVoices().find((v) => v.lang?.toLowerCase().startsWith("de")) ?? null;
|
||||||
|
chunks.forEach((c, i) => {
|
||||||
|
const u = new SpeechSynthesisUtterance(c.trim());
|
||||||
|
u.lang = "de-DE";
|
||||||
|
if (voice) u.voice = voice;
|
||||||
|
if (i === chunks.length - 1)
|
||||||
|
u.onend = () => {
|
||||||
|
speaking = false;
|
||||||
|
paused = false;
|
||||||
|
};
|
||||||
|
synth.speak(u);
|
||||||
|
});
|
||||||
|
speaking = true;
|
||||||
|
paused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSpeak() {
|
||||||
|
const synth = window.speechSynthesis;
|
||||||
|
if (!speaking) {
|
||||||
|
speak();
|
||||||
|
} else if (paused) {
|
||||||
|
synth.resume();
|
||||||
|
paused = false;
|
||||||
|
} else {
|
||||||
|
synth.pause();
|
||||||
|
paused = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSpeak() {
|
||||||
|
window.speechSynthesis.cancel();
|
||||||
|
speaking = false;
|
||||||
|
paused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (typeof window !== "undefined" && "speechSynthesis" in window)
|
||||||
|
window.speechSynthesis.cancel();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -194,6 +261,28 @@
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if canSpeak}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={toggleSpeak}
|
||||||
|
class="inline-flex items-center px-2.5 py-1 text-stein-600 hover:bg-stein-50 hover:text-stein-900 transition-colors {speaking && !paused ? 'text-wald-700!' : ''}"
|
||||||
|
aria-label={!speaking ? "Vorlesen" : paused ? "Weiterlesen" : "Pause"}
|
||||||
|
title={!speaking ? "Vorlesen" : paused ? "Weiterlesen" : "Pause"}
|
||||||
|
>
|
||||||
|
<Icon icon={!speaking ? "lucide:volume-2" : paused ? "lucide:play" : "lucide:pause"} class="size-3.5" />
|
||||||
|
</button>
|
||||||
|
{#if speaking}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={stopSpeak}
|
||||||
|
class="inline-flex items-center px-2.5 py-1 text-stein-600 hover:bg-stein-50 hover:text-stein-900 transition-colors"
|
||||||
|
aria-label="Vorlesen stoppen"
|
||||||
|
title="Vorlesen stoppen"
|
||||||
|
>
|
||||||
|
<Icon icon="lucide:square" class="size-3.5" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={copyLink}
|
onclick={copyLink}
|
||||||
|
|||||||
@@ -69,9 +69,18 @@
|
|||||||
"menu": {
|
"menu": {
|
||||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 5h16M4 12h16M4 19h16\"/>"
|
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 5h16M4 12h16M4 19h16\"/>"
|
||||||
},
|
},
|
||||||
|
"heart-handshake": {
|
||||||
|
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19.414 14.414C21 12.828 22 11.5 22 9.5a5.5 5.5 0 0 0-9.591-3.676a.6.6 0 0 1-.818.001A5.5 5.5 0 0 0 2 9.5c0 2.3 1.5 4 3 5.5l5.535 5.362a2 2 0 0 0 2.879.052a2.12 2.12 0 0 0-.004-3a2.124 2.124 0 1 0 3-3a2.124 2.124 0 0 0 3.004 0a2 2 0 0 0 0-2.828l-1.881-1.882a2.41 2.41 0 0 0-3.409 0l-1.71 1.71a2 2 0 0 1-2.828 0a2 2 0 0 1 0-2.828l2.823-2.762\"/>"
|
||||||
|
},
|
||||||
|
"calendar-plus": {
|
||||||
|
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M16 19h6M16 2v4m3 10v6m2-9.402V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8.5M3 10h18M8 2v4\"/>"
|
||||||
|
},
|
||||||
"message-circle": {
|
"message-circle": {
|
||||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092a10 10 0 1 0-4.777-4.719\"/>"
|
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092a10 10 0 1 0-4.777-4.719\"/>"
|
||||||
},
|
},
|
||||||
|
"square": {
|
||||||
|
"body": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" rx=\"2\"/>"
|
||||||
|
},
|
||||||
"printer": {
|
"printer": {
|
||||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6\"/><rect width=\"12\" height=\"8\" x=\"6\" y=\"14\" rx=\"1\"/></g>"
|
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6\"/><rect width=\"12\" height=\"8\" x=\"6\" y=\"14\" rx=\"1\"/></g>"
|
||||||
},
|
},
|
||||||
@@ -132,9 +141,6 @@
|
|||||||
"plus": {
|
"plus": {
|
||||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-7-7v14\"/>"
|
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-7-7v14\"/>"
|
||||||
},
|
},
|
||||||
"calendar-plus": {
|
|
||||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M16 19h6M16 2v4m3 10v6m2-9.402V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8.5M3 10h18M8 2v4\"/>"
|
|
||||||
},
|
|
||||||
"youtube": {
|
"youtube": {
|
||||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M2.5 17a24.1 24.1 0 0 1 0-10a2 2 0 0 1 1.4-1.4a49.6 49.6 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.1 24.1 0 0 1 0 10a2 2 0 0 1-1.4 1.4a49.6 49.6 0 0 1-16.2 0A2 2 0 0 1 2.5 17\"/><path d=\"m10 15l5-3l-5-3z\"/></g>",
|
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M2.5 17a24.1 24.1 0 0 1 0-10a2 2 0 0 1 1.4-1.4a49.6 49.6 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.1 24.1 0 0 1 0 10a2 2 0 0 1-1.4 1.4a49.6 49.6 0 0 1-16.2 0A2 2 0 0 1 2.5 17\"/><path d=\"m10 15l5-3l-5-3z\"/></g>",
|
||||||
"hidden": true
|
"hidden": true
|
||||||
|
|||||||
Reference in New Issue
Block a user