|
|
@@ -42,19 +42,19 @@
|
|
|
z-index: 10001;
|
|
|
}
|
|
|
|
|
|
- .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 {
|
|
|
+ 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;
|
|
|
@@ -73,30 +73,30 @@
|
|
|
background-color: #dc3545;
|
|
|
}
|
|
|
|
|
|
- .button-pause {
|
|
|
- background-color: #ffc107;
|
|
|
- color: black;
|
|
|
- }
|
|
|
-
|
|
|
- .button-stop {
|
|
|
- background-color: #dc3545;
|
|
|
- }
|
|
|
+ .button-pause {
|
|
|
+ background-color: #ffc107;
|
|
|
+ color: black;
|
|
|
+ }
|
|
|
+
|
|
|
+ .button-stop {
|
|
|
+ background-color: #dc3545;
|
|
|
+ }
|
|
|
|
|
|
/* 底部按钮栏 */
|
|
|
- .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;
|
|
|
- }
|
|
|
+ .bottom-bar {
|
|
|
+ position: fixed;
|
|
|
+ bottom: 0;
|
|
|
+ margin: auto;
|
|
|
+ width: 75%;
|
|
|
+ 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 {
|
|
|
@@ -159,17 +159,17 @@
|
|
|
}
|
|
|
|
|
|
/* 手机端优化 */
|
|
|
- @media (max-width: 768px) {
|
|
|
+ @media (max-width: 768px) {
|
|
|
.modal {
|
|
|
width: 95%;
|
|
|
padding: 10px;
|
|
|
}
|
|
|
|
|
|
- .button {
|
|
|
- font-size: 10px;
|
|
|
- padding: 4px 4px;
|
|
|
- margin: 0 2px;
|
|
|
- }
|
|
|
+ .button {
|
|
|
+ font-size: 10px;
|
|
|
+ padding: 4px 4px;
|
|
|
+ margin: 0 2px;
|
|
|
+ }
|
|
|
|
|
|
.bottom-bar {
|
|
|
padding: 6px 0;
|
|
|
@@ -188,14 +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>
|
|
|
- <button id="stop-play-button" class="button button-stop">停止</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="pause-play-button" class="button button-pause">暂停播放</button>
|
|
|
+ <button id="stop-play-button" class="button button-stop">停止</button>
|
|
|
+ <button id="toggle-text-select" class="button button-success">文本选择</button>
|
|
|
+ </div>
|
|
|
|
|
|
<input type="file" id="pdfInput" accept="application/pdf" style="display: none;" />
|
|
|
|
|
|
@@ -1045,18 +1045,72 @@
|
|
|
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 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 bottomBar = document.querySelector('.bottom-bar');
|
|
|
+
|
|
|
+ let selectedFile = null;
|
|
|
+ let currentPage = 1;
|
|
|
const itemsPerPage = 10;
|
|
|
let allFiles = [];
|
|
|
- let selectedStartWord = '';
|
|
|
+ let selectedStartContext = null;
|
|
|
+
|
|
|
+ function syncBottomBarToPage() {
|
|
|
+ if (!bottomBar) return;
|
|
|
+
|
|
|
+ const app = window.PDFViewerApplication;
|
|
|
+ const currentPageNumber = app && app.pdfViewer ? app.pdfViewer.currentPageNumber : null;
|
|
|
+ const pageEl = currentPageNumber
|
|
|
+ ? document.querySelector(`.page[data-page-number="${currentPageNumber}"]`)
|
|
|
+ : document.querySelector('.page');
|
|
|
+
|
|
|
+ if (!pageEl) {
|
|
|
+ bottomBar.style.width = '75%';
|
|
|
+ bottomBar.style.left = '50%';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const rect = pageEl.getBoundingClientRect();
|
|
|
+ if (!rect.width) return;
|
|
|
+
|
|
|
+ const viewportWidth = window.innerWidth;
|
|
|
+ const maxWidth = Math.max(240, viewportWidth - 16);
|
|
|
+ const width = Math.min(rect.width, maxWidth);
|
|
|
+ bottomBar.style.width = `${Math.round(width)}px`;
|
|
|
+
|
|
|
+ let centerX = rect.left + rect.width / 2;
|
|
|
+ const minCenter = width / 2 + 8;
|
|
|
+ const maxCenter = viewportWidth - width / 2 - 8;
|
|
|
+ centerX = Math.min(maxCenter, Math.max(minCenter, centerX));
|
|
|
+ bottomBar.style.left = `${Math.round(centerX)}px`;
|
|
|
+ }
|
|
|
+
|
|
|
+ function setupBottomBarSync() {
|
|
|
+ const viewerContainer = document.getElementById('viewerContainer');
|
|
|
+ if (viewerContainer) {
|
|
|
+ viewerContainer.addEventListener('scroll', () => requestAnimationFrame(syncBottomBarToPage), { passive: true });
|
|
|
+ }
|
|
|
+ window.addEventListener('resize', () => requestAnimationFrame(syncBottomBarToPage), { passive: true });
|
|
|
+
|
|
|
+ const tryBindPdfEvents = () => {
|
|
|
+ const app = window.PDFViewerApplication;
|
|
|
+ const eventBus = app ? app.eventBus : null;
|
|
|
+ if (!eventBus) {
|
|
|
+ setTimeout(tryBindPdfEvents, 200);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ ['pagechanging', 'scalechanging', 'updateviewarea', 'pagerendered'].forEach(evt => {
|
|
|
+ eventBus.on(evt, () => requestAnimationFrame(syncBottomBarToPage));
|
|
|
+ });
|
|
|
+ requestAnimationFrame(syncBottomBarToPage);
|
|
|
+ };
|
|
|
+ tryBindPdfEvents();
|
|
|
+ }
|
|
|
+ setupBottomBarSync();
|
|
|
|
|
|
// 上传按钮点击事件
|
|
|
uploadButton.addEventListener('click', function () {
|
|
|
@@ -1110,16 +1164,16 @@
|
|
|
formData.append('file', selectedFile);
|
|
|
formData.append('custom_name', customFileName);
|
|
|
|
|
|
- fetch('/upload-pdf', {
|
|
|
- method: 'POST',
|
|
|
- body: formData
|
|
|
- })
|
|
|
- .then(response => {
|
|
|
- if (!response.ok) {
|
|
|
- throw new Error(`HTTP ${response.status}`);
|
|
|
- }
|
|
|
- return response.json();
|
|
|
- })
|
|
|
+ fetch('/upload-pdf', {
|
|
|
+ method: 'POST',
|
|
|
+ body: formData
|
|
|
+ })
|
|
|
+ .then(response => {
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`HTTP ${response.status}`);
|
|
|
+ }
|
|
|
+ return response.json();
|
|
|
+ })
|
|
|
.then(data => {
|
|
|
if (data.success) {
|
|
|
const uploadedFilePath = data.file_path;
|
|
|
@@ -1178,13 +1232,13 @@
|
|
|
directoryModal.style.display = 'block';
|
|
|
currentPage = 1;
|
|
|
|
|
|
- fetch('/list-pdfs')
|
|
|
- .then(response => {
|
|
|
- if (!response.ok) {
|
|
|
- throw new Error(`HTTP ${response.status}`);
|
|
|
- }
|
|
|
- return response.json();
|
|
|
- })
|
|
|
+ fetch('/list-pdfs')
|
|
|
+ .then(response => {
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`HTTP ${response.status}`);
|
|
|
+ }
|
|
|
+ return response.json();
|
|
|
+ })
|
|
|
.then(data => {
|
|
|
if (data.success) {
|
|
|
allFiles = data.files;
|
|
|
@@ -1234,12 +1288,12 @@
|
|
|
});
|
|
|
|
|
|
// 语音控制
|
|
|
- let isListening = false;
|
|
|
- let audioContext = null;
|
|
|
- let isPaused = false;
|
|
|
- let currentAudio = null;
|
|
|
- let activePlayController = null;
|
|
|
- let activeStreamReader = null;
|
|
|
+ let isListening = false;
|
|
|
+ let audioContext = null;
|
|
|
+ let isPaused = false;
|
|
|
+ let currentAudio = null;
|
|
|
+ let activePlayController = null;
|
|
|
+ let activeStreamReader = null;
|
|
|
|
|
|
// 初始化 AudioContext
|
|
|
function initAudioContext() {
|
|
|
@@ -1273,84 +1327,127 @@
|
|
|
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();
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 停止播放
|
|
|
- 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();
|
|
|
+ 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();
|
|
|
+ });
|
|
|
+
|
|
|
+ function countOccurrences(haystack, needle) {
|
|
|
+ if (!haystack || !needle) return 0;
|
|
|
+ let count = 0;
|
|
|
+ let pos = 0;
|
|
|
+ const source = haystack.toLowerCase();
|
|
|
+ const target = needle.toLowerCase();
|
|
|
+ while (true) {
|
|
|
+ const idx = source.indexOf(target, pos);
|
|
|
+ if (idx === -1) break;
|
|
|
+ count++;
|
|
|
+ pos = idx + target.length;
|
|
|
+ }
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+
|
|
|
+ function findNthOccurrence(haystack, needle, n) {
|
|
|
+ if (!haystack || !needle || !n || n < 1) return -1;
|
|
|
+ let pos = 0;
|
|
|
+ let seen = 0;
|
|
|
+ const source = haystack.toLowerCase();
|
|
|
+ const target = needle.toLowerCase();
|
|
|
+ while (true) {
|
|
|
+ const idx = source.indexOf(target, pos);
|
|
|
+ if (idx === -1) return -1;
|
|
|
+ seen++;
|
|
|
+ if (seen === n) return idx;
|
|
|
+ pos = idx + target.length;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 仅识别 PDF 页面文字层里的选择,并记录是该页第几次出现
|
|
|
+ function getSelectedContextOnPage() {
|
|
|
+ const selection = window.getSelection();
|
|
|
+ if (!selection || selection.rangeCount === 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const selectedText = selection.toString().trim();
|
|
|
+ if (!selectedText) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const range = selection.getRangeAt(0);
|
|
|
+ const container = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
|
|
|
+ ? range.commonAncestorContainer
|
|
|
+ : range.commonAncestorContainer.parentElement;
|
|
|
+ if (!container || !container.closest('.textLayer')) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const textLayer = container.closest('.textLayer');
|
|
|
+ const firstWord = selectedText.split(/\s+/)[0].trim();
|
|
|
+ const word = firstWord.replace(/(^[^A-Za-z0-9\u4e00-\u9fa5]+|[^A-Za-z0-9\u4e00-\u9fa5]+$)/g, '') || firstWord;
|
|
|
+ if (!word) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const prefixRange = document.createRange();
|
|
|
+ prefixRange.selectNodeContents(textLayer);
|
|
|
+ prefixRange.setEnd(range.startContainer, range.startOffset);
|
|
|
+ const prefixText = prefixRange.toString();
|
|
|
+
|
|
|
+ // 当前词是“前缀中已出现次数 + 1”
|
|
|
+ const occurrence = countOccurrences(prefixText, word) + 1;
|
|
|
+ return { word, occurrence };
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateReadPageButtonText() {
|
|
|
+ selectedStartContext = getSelectedContextOnPage();
|
|
|
+ readPageButton.textContent = selectedStartContext ? '从此阅读' : '阅读整页';
|
|
|
+ }
|
|
|
+ document.addEventListener('selectionchange', updateReadPageButtonText);
|
|
|
+ updateReadPageButtonText();
|
|
|
|
|
|
// 播放音频队列(添加句子间停顿以模拟段落停顿)
|
|
|
async function playAudioSequentially(queue, signal) {
|
|
|
@@ -1458,29 +1555,29 @@
|
|
|
queue.splice(index, 0, chunk);
|
|
|
}
|
|
|
|
|
|
- // 开始转换(发送完整文本,让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 })
|
|
|
- });
|
|
|
+ // 开始转换(发送完整文本,让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();
|
|
|
- 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);
|
|
|
+ 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();
|
|
|
@@ -1511,21 +1608,21 @@
|
|
|
}
|
|
|
|
|
|
audioQueue.push(null); // 结束信号
|
|
|
- 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';
|
|
|
- }
|
|
|
- }
|
|
|
+ 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', () => {
|
|
|
@@ -1556,41 +1653,39 @@
|
|
|
});
|
|
|
|
|
|
// 阅读整页
|
|
|
- readPageButton.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();
|
|
|
-
|
|
|
- // 提取完整文本,保留标点符号(不预分割)
|
|
|
- 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;
|
|
|
- }
|
|
|
+ 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 startContext = selectedStartContext || getSelectedContextOnPage();
|
|
|
+ if (startContext?.word) {
|
|
|
+ const startIndex = findNthOccurrence(fullText, startContext.word, startContext.occurrence);
|
|
|
+ if (startIndex >= 0) {
|
|
|
+ fullText = fullText.slice(startIndex).trim();
|
|
|
+ } else {
|
|
|
+ loadingIndicator.textContent = `未在当前页找到对应位置词:${startContext.word},将从页首阅读`;
|
|
|
+ loadingIndicator.style.display = 'block';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (fullText.length === 0) {
|
|
|
+ loadingIndicator.textContent = '页面无文本内容';
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
await startConversion(fullText);
|
|
|
} catch (error) {
|
|
|
@@ -1601,4 +1696,4 @@
|
|
|
});
|
|
|
</script>
|
|
|
</body>
|
|
|
-</html>
|
|
|
+</html>
|