refactor: redesign UI — article title at top, fix list display, larger fonts
- Move article title/source/meta OUT of player into .brief-now-playing section at top - .brief-player now contains only wave canvas + play button (prevents overflow issue) - Increase font sizes: title 18→20px, list 13→14px, top bar 13→14px - Add HTML escaping (esc()) in renderList to prevent XSS/broken layout - Fix skip() to use articles.length (not speechQueue.length) for bounds - Guard updateListHighlight against null querySelector results - Remove defer from inline script (ignored for inline scripts, was misleading) - Add border-top to brief-list for clear visual separation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e58278e3ca
commit
0c81dcf06f
344
index.html
344
index.html
|
|
@ -39,9 +39,7 @@
|
|||
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
|
||||
|
||||
<style>
|
||||
/* ── base.css の main スタイルを完全上書き ───────────────────────
|
||||
base.css: padding:24px 20px / max-width:860px / margin:0 auto
|
||||
Brief は没入型プレーヤーのため自前でレイアウト管理する */
|
||||
/* ── base.css の main を没入型レイアウト用に上書き ── */
|
||||
main {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
|
|
@ -53,33 +51,30 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
/* ── トップバー ─────────────────────────────────────────────── */
|
||||
/* ── トップバー ────────────────────────────────────── */
|
||||
.brief-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: max(16px, env(safe-area-inset-top)) 16px 12px;
|
||||
padding: max(14px, env(safe-area-inset-top)) 16px 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.brief-date {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--text2);
|
||||
}
|
||||
.brief-sep { color: var(--border); font-size: 13px; }
|
||||
.brief-count {
|
||||
font-size: 13px;
|
||||
color: var(--text3);
|
||||
}
|
||||
.brief-sep { color: var(--border); font-size: 14px; }
|
||||
.brief-count { font-size: 14px; color: var(--text3); }
|
||||
.brief-top-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
}
|
||||
.brief-icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
|
@ -92,7 +87,38 @@
|
|||
}
|
||||
.brief-icon-btn:active { background: var(--surface); color: var(--text2); }
|
||||
|
||||
/* ── プレーヤー本体(flex: 1) ──────────────────────────────── */
|
||||
/* ── 現在再生中の記事情報(上部に固定配置) ──────── */
|
||||
.brief-now-playing {
|
||||
flex-shrink: 0;
|
||||
padding: 14px 20px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
min-height: 80px;
|
||||
}
|
||||
.brief-article-meta {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.brief-article-title {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.brief-article-source {
|
||||
font-size: 13px;
|
||||
color: var(--text3);
|
||||
}
|
||||
|
||||
/* ── プレーヤー(波形 + 再生ボタンのみ)flex: 1 ─── */
|
||||
.brief-player {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
|
@ -101,42 +127,15 @@
|
|||
justify-content: center;
|
||||
gap: 0;
|
||||
padding: 0 24px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── 記事情報 ────────────────────────────────────────────────── */
|
||||
.brief-article-meta {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--accent);
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.brief-article-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
line-height: 1.45;
|
||||
text-align: center;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
min-height: 100px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.brief-article-source {
|
||||
font-size: 12px;
|
||||
color: var(--text3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── 波形キャンバス ─────────────────────────────────────────── */
|
||||
/* ── 波形キャンバス ─────────────────────────────── */
|
||||
.brief-wave-wrap {
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
margin: 28px 0 24px;
|
||||
height: 56px;
|
||||
margin: 0 0 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -147,7 +146,7 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
/* ── 再生ボタン ─────────────────────────────────────────────── */
|
||||
/* ── 再生ボタン ──────────────────────────────────── */
|
||||
.brief-play-btn {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
|
|
@ -169,51 +168,55 @@
|
|||
}
|
||||
@keyframes playGlow {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(110,231,183,0.35); }
|
||||
50% { box-shadow: 0 0 0 12px rgba(110,231,183,0); }
|
||||
50% { box-shadow: 0 0 0 14px rgba(110,231,183,0); }
|
||||
}
|
||||
.brief-progress {
|
||||
font-size: 12px;
|
||||
color: var(--text3);
|
||||
text-align: center;
|
||||
margin-top: 14px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
/* ── コントロールバー ────────────────────────────────────────── */
|
||||
/* ── コントロールバー ────────────────────────────── */
|
||||
.brief-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
padding: 16px 24px max(20px, env(safe-area-inset-bottom));
|
||||
padding: 10px 24px 10px;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.brief-ctrl-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 48px;
|
||||
height: 44px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius);
|
||||
transition: background 0.12s, color 0.12s;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
gap: 4px;
|
||||
}
|
||||
.brief-ctrl-btn:active { background: var(--surface); color: var(--text); }
|
||||
.brief-ctrl-btn.disabled { opacity: 0.3; pointer-events: none; }
|
||||
.brief-speed-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
gap: 4px;
|
||||
flex: 2;
|
||||
justify-content: center;
|
||||
}
|
||||
.brief-speed-btn {
|
||||
padding: 6px 8px;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text3);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
|
|
@ -225,20 +228,68 @@
|
|||
background: var(--surface);
|
||||
}
|
||||
|
||||
/* ── 読み込み中・エラー ─────────────────────────────────────── */
|
||||
.brief-status {
|
||||
font-size: 13px;
|
||||
color: var(--text3);
|
||||
text-align: center;
|
||||
/* ── 記事リスト ───────────────────────────────────── */
|
||||
.brief-list {
|
||||
flex-shrink: 0;
|
||||
max-height: 42dvh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
||||
}
|
||||
.brief-progress {
|
||||
font-size: 11px;
|
||||
.brief-list::-webkit-scrollbar { display: none; }
|
||||
.brief-list-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
.brief-list-item:active { background: var(--surface); }
|
||||
.brief-list-item.active { background: var(--surface); }
|
||||
.brief-list-num {
|
||||
font-size: 12px;
|
||||
color: var(--text3);
|
||||
text-align: center;
|
||||
margin-top: 6px;
|
||||
width: 18px;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
padding-top: 2px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.brief-list-num.active { color: var(--accent); font-weight: 600; }
|
||||
.brief-list-info { flex: 1; min-width: 0; }
|
||||
.brief-list-title {
|
||||
font-size: 14px;
|
||||
color: var(--text2);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.brief-list-title.active { color: var(--text); font-weight: 500; }
|
||||
.brief-list-source {
|
||||
font-size: 12px;
|
||||
color: var(--text3);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ── 設定パネル内の入力 ─────────────────────────────────────── */
|
||||
/* ── 音声エンジン表示 ───────────────────────────── */
|
||||
.voice-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 10px;
|
||||
color: var(--text3);
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
}
|
||||
.voice-badge.voicevox { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
/* ── 設定パネル内の入力 ─────────────────────────── */
|
||||
.settings-text-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
|
@ -273,66 +324,6 @@
|
|||
line-height: 1.5;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ── 記事リスト ─────────────────────────────────────────────── */
|
||||
.brief-list {
|
||||
flex-shrink: 0;
|
||||
max-height: 38dvh;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid var(--border);
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.brief-list::-webkit-scrollbar { display: none; }
|
||||
.brief-list-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
.brief-list-item:active { background: var(--surface); }
|
||||
.brief-list-item.active { background: var(--surface); }
|
||||
.brief-list-num {
|
||||
font-size: 11px;
|
||||
color: var(--text3);
|
||||
width: 16px;
|
||||
flex-shrink: 0;
|
||||
padding-top: 2px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.brief-list-num.active { color: var(--accent); }
|
||||
.brief-list-info { flex: 1; min-width: 0; }
|
||||
.brief-list-title {
|
||||
font-size: 13px;
|
||||
color: var(--text2);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.brief-list-title.active { color: var(--text); font-weight: 500; }
|
||||
.brief-list-source {
|
||||
font-size: 11px;
|
||||
color: var(--text3);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ── 音声エンジン表示 ───────────────────────────────────────── */
|
||||
.voice-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 10px;
|
||||
color: var(--text3);
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.voice-badge.voicevox { border-color: var(--accent); color: var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -400,12 +391,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- プレーヤー -->
|
||||
<div class="brief-player" id="briefPlayer">
|
||||
|
||||
<!-- 現在再生中の記事情報(最上部) -->
|
||||
<div class="brief-now-playing">
|
||||
<div class="brief-article-meta" id="briefMeta">Posimai Brief</div>
|
||||
<div class="brief-article-title" id="briefTitle">記事を読み込んでいます</div>
|
||||
<div class="brief-article-source" id="briefSource"></div>
|
||||
</div>
|
||||
|
||||
<!-- プレーヤー(波形 + 再生ボタン) -->
|
||||
<div class="brief-player" id="briefPlayer">
|
||||
|
||||
<div class="brief-wave-wrap">
|
||||
<canvas id="waveCanvas"></canvas>
|
||||
|
|
@ -445,7 +439,7 @@
|
|||
<div id="toast" role="status" aria-live="polite"></div>
|
||||
|
||||
<script src="https://posimai-ui.vercel.app/v1/base.js" defer></script>
|
||||
<script defer>
|
||||
<script>
|
||||
// ============================================================
|
||||
// Posimai Brief — 音声ニュースブリーフィング
|
||||
// ============================================================
|
||||
|
|
@ -456,14 +450,14 @@ const TTS_API = API_BASE + '/tts';
|
|||
const DAYS_JP = ['日','月','火','水','木','金','土'];
|
||||
|
||||
// ── 状態 ─────────────────────────────────────────────────────
|
||||
let articles = []; // 生記事リスト
|
||||
let speechQueue = []; // 読み上げテキスト(記事ごと)
|
||||
let currentIdx = 0; // 現在の発話インデックス
|
||||
let articles = [];
|
||||
let speechQueue = [];
|
||||
let currentIdx = 0;
|
||||
let isPlaying = false;
|
||||
let speechRate = 1.0;
|
||||
let audioCtx = null;
|
||||
let currentSource = null; // VOICEVOX AudioBufferSourceNode
|
||||
let waveRAF = null; // requestAnimationFrame handle
|
||||
let currentSource = null;
|
||||
let waveRAF = null;
|
||||
|
||||
// ── 波形データ ────────────────────────────────────────────────
|
||||
const BAR_COUNT = 64;
|
||||
|
|
@ -478,12 +472,9 @@ function updateWave(playing) {
|
|||
wavePhase += playing ? 0.06 : 0;
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
if (playing) {
|
||||
// 波のエンベロープ(全体に緩やかな山)
|
||||
const env = 0.15 + 0.5 * Math.abs(Math.sin(wavePhase - i * 0.18));
|
||||
// ランダムスパイク(鋭いパルス)
|
||||
if (Math.random() < 0.07) barT[i] = env + Math.random() * (1 - env);
|
||||
else if (Math.random() < 0.1) barT[i] = 0.04 + Math.random() * 0.12;
|
||||
// スパークル
|
||||
if (Math.random() < 0.007) barSpk[i] = 1.0;
|
||||
} else {
|
||||
if (Math.random() < 0.04) barT[i] = 0.02 + Math.random() * 0.05;
|
||||
|
|
@ -517,7 +508,6 @@ function drawWave(playing) {
|
|||
const y = (H - h) / 2;
|
||||
|
||||
if (spk > 0.12) {
|
||||
// スパーク: アクセント → 白へ
|
||||
const t = spk;
|
||||
const rv = Math.round(110 + (255 - 110) * t);
|
||||
const gv = Math.round(231 + (255 - 231) * t);
|
||||
|
|
@ -545,13 +535,11 @@ function getAudioCtx() {
|
|||
}
|
||||
|
||||
function stopAudio() {
|
||||
// VOICEVOX 停止
|
||||
if (currentSource) {
|
||||
currentSource.onended = null;
|
||||
try { currentSource.stop(); } catch(e) {}
|
||||
currentSource = null;
|
||||
}
|
||||
// ブラウザ TTS 停止
|
||||
window.speechSynthesis.cancel();
|
||||
}
|
||||
|
||||
|
|
@ -564,12 +552,11 @@ function preprocessText(t) {
|
|||
.trim();
|
||||
}
|
||||
|
||||
// VOICEVOX TTS — 成功したら Promise が解決(false を返したら fallback)
|
||||
async function tryVoicevox(text) {
|
||||
const apiKey = localStorage.getItem('posimai-brief-apikey');
|
||||
if (!apiKey) return false;
|
||||
|
||||
const ctx = getAudioCtx(); // await より前に呼ぶ(Chrome autoplay policy)
|
||||
const ctx = getAudioCtx();
|
||||
|
||||
const res = await fetch(TTS_API, {
|
||||
method: 'POST',
|
||||
|
|
@ -579,7 +566,7 @@ async function tryVoicevox(text) {
|
|||
if (!res.ok) return false;
|
||||
|
||||
const buf = await ctx.decodeAudioData(await res.arrayBuffer());
|
||||
if (!isPlaying) return true; // 停止中なら再生しない(でも成功扱い)
|
||||
if (!isPlaying) return true;
|
||||
|
||||
return new Promise(resolve => {
|
||||
currentSource = ctx.createBufferSource();
|
||||
|
|
@ -594,10 +581,8 @@ async function tryVoicevox(text) {
|
|||
});
|
||||
}
|
||||
|
||||
// ブラウザ TTS
|
||||
function speakBrowser(text, onEnd) {
|
||||
const utter = new SpeechSynthesisUtterance(preprocessText(text));
|
||||
// 日本語音声を優先選択
|
||||
const voices = speechSynthesis.getVoices();
|
||||
const ja = voices.find(v => v.lang.startsWith('ja') && v.localService)
|
||||
|| voices.find(v => v.lang.startsWith('ja'))
|
||||
|
|
@ -611,7 +596,6 @@ function speakBrowser(text, onEnd) {
|
|||
updateVoiceBadge(false);
|
||||
}
|
||||
|
||||
// 1文を話す(VOICEVOX 優先 → ブラウザ fallback、重複なし)
|
||||
async function speak(text) {
|
||||
if (!isPlaying) return;
|
||||
stopAudio();
|
||||
|
|
@ -626,11 +610,8 @@ async function speak(text) {
|
|||
|
||||
if (!isPlaying) return;
|
||||
if (voicevoxOk) {
|
||||
// VOICEVOX onended → playNext() は tryVoicevox 内の Promise.resolve で呼ばれる
|
||||
// ここで awaitが戻ってきたとき=再生完了なので次へ
|
||||
onSpeakEnd();
|
||||
} else {
|
||||
// ブラウザ TTS fallback
|
||||
speakBrowser(text, () => { if (isPlaying) onSpeakEnd(); });
|
||||
}
|
||||
}
|
||||
|
|
@ -664,6 +645,14 @@ function updatePlayBtn() {
|
|||
lucide.createIcons();
|
||||
}
|
||||
|
||||
function esc(str) {
|
||||
return (str || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function updateArticleDisplay(a, idx) {
|
||||
if (!a) return;
|
||||
const total = articles.length;
|
||||
|
|
@ -673,7 +662,6 @@ function updateArticleDisplay(a, idx) {
|
|||
document.getElementById('briefSource').textContent = a.source || '';
|
||||
document.getElementById('briefProgress').textContent = '';
|
||||
updateListHighlight(idx);
|
||||
// MediaSession
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: a.title || 'Posimai Brief',
|
||||
|
|
@ -688,19 +676,25 @@ function renderList() {
|
|||
list.innerHTML = '';
|
||||
articles.forEach((a, i) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'brief-list-item' + (i === currentIdx ? ' active' : '');
|
||||
const isActive = i === currentIdx;
|
||||
item.className = 'brief-list-item' + (isActive ? ' active' : '');
|
||||
item.innerHTML = `
|
||||
<span class="brief-list-num${i === currentIdx ? ' active' : ''}">${i + 1}</span>
|
||||
<span class="brief-list-num${isActive ? ' active' : ''}">${i + 1}</span>
|
||||
<div class="brief-list-info">
|
||||
<div class="brief-list-title${i === currentIdx ? ' active' : ''}">${a.title || ''}</div>
|
||||
${a.source ? `<div class="brief-list-source">${a.source}</div>` : ''}
|
||||
<div class="brief-list-title${isActive ? ' active' : ''}">${esc(a.title)}</div>
|
||||
${a.source ? `<div class="brief-list-source">${esc(a.source)}</div>` : ''}
|
||||
</div>`;
|
||||
item.addEventListener('click', () => {
|
||||
const wasPlaying = isPlaying;
|
||||
stopAudio();
|
||||
isPlaying = false;
|
||||
currentIdx = i;
|
||||
updateArticleDisplay(articles[i], i);
|
||||
if (wasPlaying) { isPlaying = true; updatePlayBtn(); playNext(); }
|
||||
if (wasPlaying) {
|
||||
isPlaying = true;
|
||||
updatePlayBtn();
|
||||
playNext();
|
||||
}
|
||||
});
|
||||
list.appendChild(item);
|
||||
});
|
||||
|
|
@ -710,8 +704,10 @@ function updateListHighlight(idx) {
|
|||
document.querySelectorAll('.brief-list-item').forEach((el, i) => {
|
||||
const active = i === idx;
|
||||
el.classList.toggle('active', active);
|
||||
el.querySelector('.brief-list-num').classList.toggle('active', active);
|
||||
el.querySelector('.brief-list-title').classList.toggle('active', active);
|
||||
const numEl = el.querySelector('.brief-list-num');
|
||||
const titleEl = el.querySelector('.brief-list-title');
|
||||
if (numEl) numEl.classList.toggle('active', active);
|
||||
if (titleEl) titleEl.classList.toggle('active', active);
|
||||
if (active) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
});
|
||||
}
|
||||
|
|
@ -740,15 +736,20 @@ document.getElementById('playBtn').addEventListener('click', () => {
|
|||
function skip(dir) {
|
||||
const wasPlaying = isPlaying;
|
||||
stopAudio();
|
||||
currentIdx = Math.max(0, Math.min(speechQueue.length - 1, currentIdx + dir));
|
||||
updateArticleDisplay(articles[Math.min(currentIdx, articles.length - 1)], Math.min(currentIdx, articles.length - 1));
|
||||
if (wasPlaying) playNext();
|
||||
isPlaying = false;
|
||||
currentIdx = Math.max(0, Math.min(articles.length - 1, currentIdx + dir));
|
||||
updateArticleDisplay(articles[currentIdx], currentIdx);
|
||||
if (wasPlaying) {
|
||||
isPlaying = true;
|
||||
updatePlayBtn();
|
||||
playNext();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('prevBtn').addEventListener('click', () => skip(-1));
|
||||
document.getElementById('nextBtn').addEventListener('click', () => skip(1));
|
||||
|
||||
// MediaSession アクション登録
|
||||
// MediaSession
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.setActionHandler('play', () => { if (!isPlaying) { isPlaying = true; updatePlayBtn(); playNext(); } });
|
||||
navigator.mediaSession.setActionHandler('pause', () => { if (isPlaying) { isPlaying = false; updatePlayBtn(); stopAudio(); } });
|
||||
|
|
@ -764,14 +765,12 @@ document.querySelectorAll('.brief-speed-btn').forEach(btn => {
|
|||
btn.classList.add('active');
|
||||
localStorage.setItem('posimai-brief-rate', String(speechRate));
|
||||
if (isPlaying) {
|
||||
// 現在の記事を最初から読み直す
|
||||
stopAudio();
|
||||
playNext();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 速度を復元
|
||||
const savedRate = parseFloat(localStorage.getItem('posimai-brief-rate') || '1.0');
|
||||
speechRate = savedRate;
|
||||
document.querySelectorAll('.brief-speed-btn').forEach(b => {
|
||||
|
|
@ -783,7 +782,7 @@ document.getElementById('apiKeyInput').value = localStorage.getItem('posimai-bri
|
|||
document.getElementById('apiKeySave').addEventListener('click', () => {
|
||||
const v = document.getElementById('apiKeyInput').value.trim();
|
||||
localStorage.setItem('posimai-brief-apikey', v);
|
||||
showToast('保存しました');
|
||||
if (typeof showToast === 'function') showToast('保存しました');
|
||||
});
|
||||
|
||||
// ── 日付表示 ─────────────────────────────────────────────────
|
||||
|
|
@ -799,7 +798,9 @@ function updateDate() {
|
|||
async function loadFeed() {
|
||||
document.getElementById('briefTitle').textContent = '記事を読み込んでいます';
|
||||
document.getElementById('briefSource').textContent = '';
|
||||
document.getElementById('briefMeta').textContent = 'Posimai Brief';
|
||||
document.getElementById('briefCount').textContent = '...';
|
||||
document.getElementById('briefList').innerHTML = '';
|
||||
|
||||
try {
|
||||
const res = await fetch(FEED_API, { cache: 'no-store' });
|
||||
|
|
@ -825,6 +826,7 @@ async function loadFeed() {
|
|||
} catch (e) {
|
||||
document.getElementById('briefTitle').textContent = '記事の取得に失敗しました';
|
||||
document.getElementById('briefSource').textContent = 'ネットワークを確認してください';
|
||||
document.getElementById('briefMeta').textContent = 'ERROR';
|
||||
document.getElementById('briefCount').textContent = 'エラー';
|
||||
}
|
||||
}
|
||||
|
|
@ -835,14 +837,6 @@ document.getElementById('refreshBtn').addEventListener('click', () => {
|
|||
loadFeed();
|
||||
});
|
||||
|
||||
// ── トースト ─────────────────────────────────────────────────
|
||||
function showToast(msg) {
|
||||
const el = document.getElementById('toast');
|
||||
el.textContent = msg;
|
||||
el.classList.add('show');
|
||||
setTimeout(() => el.classList.remove('show'), 2200);
|
||||
}
|
||||
|
||||
// ── SW 登録 ──────────────────────────────────────────────────
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
||||
|
|
|
|||
Loading…
Reference in New Issue