Procházet zdrojové kódy

当前起点阅读

root před 6 hodinami
rodič
revize
61e2dc98fc
1 změnil soubory, kde provedl 207 přidání a 107 odebrání
  1. 207 107
      static/web/viewer.html

+ 207 - 107
static/web/viewer.html

@@ -42,18 +42,19 @@
             z-index: 10001;
         }
 
-        .button {
-            padding: 6px 12px;
-            background-color: #007bff;
-            color: white;
-            border: none;
-            border-radius: 5px;
-            cursor: pointer;
-            font-size: 12px;
-            margin: 0 4px;
-            flex: 1;
-            text-align: center;
-        }
+        .button {
+            padding: 4px 6px;
+            background-color: #007bff;
+            color: white;
+            border: none;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 11px;
+            margin: 0 2px;
+            flex: 1;
+            text-align: center;
+            white-space: nowrap;
+        }
 
         .button-secondary {
             background-color: #6c757d;
@@ -72,26 +73,30 @@
             background-color: #dc3545;
         }
 
-        .button-pause {
-            background-color: #ffc107;
-            color: black;
-        }
+        .button-pause {
+            background-color: #ffc107;
+            color: black;
+        }
+
+        .button-stop {
+            background-color: #dc3545;
+        }
 
         /* 底部按钮栏 */
-        .bottom-bar {
-            position: fixed;
-            bottom: 0;
-            margin: auto;
-            width: 60%;
-            left: 50%;
-            transform: translateX(-50%);
-            background-color: rgba(255, 255, 255, 0.0);
-            display: flex;
-            justify-content: space-around;
-            padding: 8px 0;
-            box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
-            z-index: 1000;
-        }
+        .bottom-bar {
+            position: fixed;
+            bottom: 0;
+            margin: auto;
+            width: 92%;
+            left: 50%;
+            transform: translateX(-50%);
+            background-color: rgba(255, 255, 255, 0.0);
+            display: flex;
+            justify-content: space-around;
+            padding: 6px 0;
+            box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
+            z-index: 1000;
+        }
 
         /* 顶部状态条 */
         #loading-indicator {
@@ -154,17 +159,17 @@
         }
 
         /* 手机端优化 */
-        @media (max-width: 768px) {
+        @media (max-width: 768px) {
             .modal {
                 width: 95%;
                 padding: 10px;
             }
 
-            .button {
-                font-size: 12px;
-                padding: 6px 8px;
-                margin: 0 2px;
-            }
+            .button {
+                font-size: 10px;
+                padding: 4px 4px;
+                margin: 0 2px;
+            }
 
             .bottom-bar {
                 padding: 6px 0;
@@ -183,13 +188,14 @@
 
     <audio id="audio-player" controls></audio>
 
-    <div class="bottom-bar">
-        <button id="uploadButton" class="button">上传 PDF</button>
-        <button id="openDirectoryButton" class="button">打开目录</button>
-        <button id="select-all-text" class="button button-light">阅读整页</button>
-        <button id="toggle-text-select" class="button button-success">文本选择</button>
-        <button id="pause-play-button" class="button button-pause">暂停播放</button>
-    </div>
+    <div class="bottom-bar">
+        <button id="uploadButton" class="button">上传 PDF</button>
+        <button id="openDirectoryButton" class="button">打开目录</button>
+        <button id="select-all-text" class="button button-light">阅读整页</button>
+        <button id="toggle-text-select" class="button button-success">文本选择</button>
+        <button id="pause-play-button" class="button button-pause">暂停播放</button>
+        <button id="stop-play-button" class="button button-stop">停止</button>
+    </div>
 
     <input type="file" id="pdfInput" accept="application/pdf" style="display: none;" />
 
@@ -1039,15 +1045,18 @@
                 const directoryModal = document.getElementById('directoryModal');
                 const closeDirectoryButton = document.getElementById('closeDirectoryButton');
                 const pdfList = document.getElementById('pdfList');
-                const prevPageButton = document.getElementById('prevPage');
-                const nextPageButton = document.getElementById('nextPage');
-                const pageInfo = document.getElementById('pageInfo');
-                const pausePlayButton = document.getElementById('pause-play-button');
-
-                let selectedFile = null;
-                let currentPage = 1;
-                const itemsPerPage = 10;
-                let allFiles = [];
+                const prevPageButton = document.getElementById('prevPage');
+                const nextPageButton = document.getElementById('nextPage');
+                const pageInfo = document.getElementById('pageInfo');
+                const readPageButton = document.getElementById('select-all-text');
+                const pausePlayButton = document.getElementById('pause-play-button');
+                const stopPlayButton = document.getElementById('stop-play-button');
+
+                let selectedFile = null;
+                let currentPage = 1;
+                const itemsPerPage = 10;
+                let allFiles = [];
+                let selectedStartWord = '';
 
                 // 上传按钮点击事件
                 uploadButton.addEventListener('click', function () {
@@ -1225,10 +1234,12 @@
                 });
 
                 // 语音控制
-                let isListening = false;
-                let audioContext = null;
-                let isPaused = false;
-                let currentAudio = null;
+                let isListening = false;
+                let audioContext = null;
+                let isPaused = false;
+                let currentAudio = null;
+                let activePlayController = null;
+                let activeStreamReader = null;
 
                 // 初始化 AudioContext
                 function initAudioContext() {
@@ -1262,18 +1273,84 @@
                     button.classList.toggle('button-danger', isListening);
                 });
 
-                // 暂停/继续播放
-                pausePlayButton.addEventListener('click', () => {
-                    isPaused = !isPaused;
-                    pausePlayButton.textContent = isPaused ? "继续播放" : "暂停播放";
-                    if (isPaused && currentAudio) {
+                // 暂停/继续播放
+                pausePlayButton.addEventListener('click', () => {
+                    isPaused = !isPaused;
+                    pausePlayButton.textContent = isPaused ? "继续播放" : "暂停播放";
+                    if (isPaused && currentAudio) {
                         currentAudio.pause();
                         if (audioContext) audioContext.suspend();
                     } else if (!isPaused && currentAudio) {
-                        currentAudio.play();
-                        if (audioContext) audioContext.resume();
-                    }
-                });
+                        currentAudio.play();
+                        if (audioContext) audioContext.resume();
+                    }
+                });
+
+                // 停止播放
+                function stopCurrentPlayback(showTip = true) {
+                    if (activePlayController) {
+                        activePlayController.abort();
+                        activePlayController = null;
+                    }
+                    if (activeStreamReader) {
+                        try {
+                            activeStreamReader.cancel();
+                        } catch (e) {
+                            console.warn('取消流读取失败:', e);
+                        }
+                        activeStreamReader = null;
+                    }
+                    if (currentAudio) {
+                        currentAudio.pause();
+                        currentAudio.src = '';
+                        currentAudio = null;
+                    }
+                    isPaused = false;
+                    pausePlayButton.textContent = "暂停播放";
+                    if (showTip) {
+                        const loadingIndicator = document.getElementById('loading-indicator');
+                        loadingIndicator.textContent = '已停止';
+                        loadingIndicator.style.display = 'block';
+                        setTimeout(() => {
+                            loadingIndicator.style.display = 'none';
+                        }, 800);
+                    }
+                }
+
+                stopPlayButton.addEventListener('click', () => {
+                    stopCurrentPlayback();
+                });
+
+                // 仅识别 PDF 页面文字层里的选择
+                function getSelectedWordOnPage() {
+                    const selection = window.getSelection();
+                    if (!selection || selection.rangeCount === 0) {
+                        return '';
+                    }
+
+                    const selectedText = selection.toString().trim();
+                    if (!selectedText) {
+                        return '';
+                    }
+
+                    const range = selection.getRangeAt(0);
+                    const container = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
+                        ? range.commonAncestorContainer
+                        : range.commonAncestorContainer.parentElement;
+                    if (!container || !container.closest('.textLayer')) {
+                        return '';
+                    }
+
+                    const firstWord = selectedText.split(/\s+/)[0].trim();
+                    return firstWord.replace(/(^[^A-Za-z0-9\u4e00-\u9fa5]+|[^A-Za-z0-9\u4e00-\u9fa5]+$)/g, '') || firstWord;
+                }
+
+                function updateReadPageButtonText() {
+                    selectedStartWord = getSelectedWordOnPage();
+                    readPageButton.textContent = selectedStartWord ? '从此阅读' : '阅读整页';
+                }
+                document.addEventListener('selectionchange', updateReadPageButtonText);
+                updateReadPageButtonText();
 
                 // 播放音频队列(添加句子间停顿以模拟段落停顿)
                 async function playAudioSequentially(queue, signal) {
@@ -1381,26 +1458,29 @@
                     queue.splice(index, 0, chunk);
                 }
 
-                // 开始转换(发送完整文本,让TTS API处理分割)
-                async function startConversion(fullText) {
-                    const loadingIndicator = document.getElementById('loading-indicator');
-                    try {
-                        const response = await fetch('http://141.140.15.30:8028/generate', {
-                            method: 'POST',
-                            headers: { 'Content-Type': 'application/json' },
-                            body: JSON.stringify({ text: fullText, voice: "af_heart", speed: 1.0 })
-                        });
+                // 开始转换(发送完整文本,让TTS API处理分割)
+                async function startConversion(fullText) {
+                    const loadingIndicator = document.getElementById('loading-indicator');
+                    try {
+                        stopCurrentPlayback(false);
+                        const response = await fetch('http://141.140.15.30:8028/generate', {
+                            method: 'POST',
+                            headers: { 'Content-Type': 'application/json' },
+                            body: JSON.stringify({ text: fullText, voice: "af_heart", speed: 1.0 })
+                        });
 
                         if (!response.ok) throw new Error(`服务器错误: ${response.status}`);
 
-                        const reader = response.body.getReader();
-                        const decoder = new TextDecoder();
-                        let buffer = '';
-                        const audioQueue = [];
-                        let expectedIndex = 0;
-
-                        const playController = new AbortController();
-                        const playPromise = playAudioSequentially(audioQueue, playController.signal);
+                        const reader = response.body.getReader();
+                        activeStreamReader = reader;
+                        const decoder = new TextDecoder();
+                        let buffer = '';
+                        const audioQueue = [];
+                        let expectedIndex = 0;
+
+                        const playController = new AbortController();
+                        activePlayController = playController;
+                        const playPromise = playAudioSequentially(audioQueue, playController.signal);
 
                         while (true) {
                             const { done, value } = await reader.read();
@@ -1431,14 +1511,21 @@
                         }
 
                         audioQueue.push(null); // 结束信号
-                        await playPromise;
-
-                    } catch (error) {
-                        console.error(error);
-                        loadingIndicator.textContent = `错误: ${error.message}`;
-                        loadingIndicator.style.display = 'block';
-                    }
-                }
+                        await playPromise;
+                        activePlayController = null;
+                        activeStreamReader = null;
+
+                    } catch (error) {
+                        activePlayController = null;
+                        activeStreamReader = null;
+                        if (error.name === 'AbortError') {
+                            return;
+                        }
+                        console.error(error);
+                        loadingIndicator.textContent = `错误: ${error.message}`;
+                        loadingIndicator.style.display = 'block';
+                    }
+                }
 
                 // 监听文本选择
                 document.addEventListener('DOMContentLoaded', () => {
@@ -1469,28 +1556,41 @@
                 });
 
                 // 阅读整页
-                document.getElementById('select-all-text').addEventListener('click', async function () {
-                    const { pdfViewer } = PDFViewerApplication;
-                    const currentPageNumber = pdfViewer.currentPageNumber;
-                    const loadingIndicator = document.getElementById('loading-indicator');
-
-                    try {
+                readPageButton.addEventListener('click', async function () {
+                    const { pdfViewer } = PDFViewerApplication;
+                    const currentPageNumber = pdfViewer.currentPageNumber;
+                    const loadingIndicator = document.getElementById('loading-indicator');
+
+                    try {
                         initAudioContext();
                         await getPlayPermission();
                         loadingIndicator.textContent = '正在提取页面文本...';
                         loadingIndicator.style.display = 'block';
 
-                        const pdfDocument = PDFViewerApplication.pdfDocument;
-                        const page = await pdfDocument.getPage(currentPageNumber);
-                        const textContent = await page.getTextContent();
-
-                        // 提取完整文本,保留标点符号(不预分割)
-                        const fullText = textContent.items.map(item => item.str).join(' ').trim();
-
-                        if (fullText.length === 0) {
-                            loadingIndicator.textContent = '页面无文本内容';
-                            return;
-                        }
+                        const pdfDocument = PDFViewerApplication.pdfDocument;
+                        const page = await pdfDocument.getPage(currentPageNumber);
+                        const textContent = await page.getTextContent();
+
+                        // 提取完整文本,保留标点符号(不预分割)
+                        let fullText = textContent.items.map(item => item.str).join(' ').trim();
+
+                        const startWord = selectedStartWord || getSelectedWordOnPage();
+                        if (startWord) {
+                            const normalizedText = fullText.toLowerCase();
+                            const normalizedStart = startWord.toLowerCase();
+                            const startIndex = normalizedText.indexOf(normalizedStart);
+                            if (startIndex >= 0) {
+                                fullText = fullText.slice(startIndex).trim();
+                            } else {
+                                loadingIndicator.textContent = `未在当前页找到起始词:${startWord},将从页首阅读`;
+                                loadingIndicator.style.display = 'block';
+                            }
+                        }
+
+                        if (fullText.length === 0) {
+                            loadingIndicator.textContent = '页面无文本内容';
+                            return;
+                        }
 
                         await startConversion(fullText);
                     } catch (error) {