7 Commits
1.0.0 ... v1.5

Author SHA1 Message Date
ElmGates
4e4d315aac change 2025-12-27 13:40:03 +08:00
ElmGates
a5f54055ea Merge branch 'main' of https://github.com/ElmGates/ticiqi 2025-12-27 13:35:56 +08:00
ElmGates
208a91ab4d upload1.5 2025-12-27 13:09:46 +08:00
ElmGates
8af1960081 Delete README.md 2025-12-27 13:09:30 +08:00
ElmGates
b806e5e30c Delete pdf提词器 directory 2025-12-27 13:09:21 +08:00
ElmGates
172f978edf upload 2025-12-27 13:07:25 +08:00
ElmGates
4966135ff5 Delete 文本提词器 directory 2025-12-27 13:04:55 +08:00
8 changed files with 474 additions and 18 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.DS_Store

View File

@@ -1,10 +1,87 @@
# 提词器源码
两种提词器网页的源码包含文本提词器和pdf提词器支持直接在本地使用但是可能需要联网。
# 使用方法
1.直接下载源码本地浏览器打开即可需要联网部分js使用的是网络链接。
2.部署到自己的服务器只要支持http访问即可。
3.使用Cloudflare Pages部署即可。
# 版权说明
本项目完全开源,可用于个人,商用请署名,二次开发请署名。
# 网页版提词器 (Web Teleprompter) v1.5
一个功能强大、简单易用的网页版提词器工具。专为演讲、视频录制、直播等场景设计,支持多设备远程协同控制。
<a href="https://teleprompter.superjia.com.cn">在线演示</a>
## ✨ 主要功能
### 📝 文本提词器 (核心版本)
包含以下特性:
* **智能文本处理**
* 支持文本粘贴与编辑。
* **智能格式化**:自动根据标点符号进行分段,优化阅读体验。
* **强制分段**:支持设置最大段落字数,防止单行过长。
* **撤销/重做**:支持撤销格式化操作。
* **专业的显示控制**
* **镜像模式**:支持横向镜像翻转,适配专业提词器玻璃。
* **样式自定义**:自由调节字体大小、行间距、文字颜色、背景颜色。
* **全屏模式**:沉浸式阅读体验。
* **平滑滚动控制**
* 可调节滚动速度 (0.1 - 200)。
* 支持暂停/开始、重置。
* 支持上一行/下一行精准微调。
* **键盘快捷键支持**
* **🤝 远程协作 (Beta)**
* 基于 P2P 技术 (PeerJS) 实现。
* **多端同步**:支持手机/平板作为遥控器控制电脑端提词器。
* **全量同步**:实时同步文本内容、滚动状态、速度设置及跳转进度。
* **简易连接**:通过 4 位数字房间 ID 快速连接。
* 无需服务器中转,隐私安全。
* **移动端适配**
* 针对手机端优化的提示与交互体验。
### 📄 PDF 提词器
位于 `pdf` 目录:
* 支持直接上传 PDF 文件。
* 自动处理 PDF 内容进行提词播放。
* 支持镜像翻转与速度控制。
## 🚀 快速开始
### 方式 1直接运行
本项目为纯静态网页,无需安装任何依赖。
1. 双击 `index.html` 在浏览器中打开即可使用。
### 方式 2部署
可以将内容部署到任何静态网页托管服务(如 GitHub Pages, Vercel, Nginx 等)。
## 🎮 操作指南
### 快捷键
* **空格键 (Space)**: 暂停 / 继续滚动
* **R 键**: 重置滚动到顶部
* **F 键**: 切换全屏模式
* **Enter 键**: 在跳转输入框中确认跳转
### 远程控制使用方法
1. **主机端(显示端)**
* 打开高级功能菜单 -> 远程协作。
* 点击“创建房间”。
* 将生成的 4 位数字 ID 发送给控制端。
2. **控制端(遥控端)**
* 打开同样的网页。
* 打开高级功能菜单 -> 远程协作。
* 输入主机端的 ID点击“加入”。
* 连接成功后,控制端的任何操作(滚动、修改文本、设置)都会实时同步到主机端。
## 📂 目录结构
```
/
├── index.html # 主程序入口
├── script.js # 核心逻辑 (含 Teleprompter 类与 RemoteController 类)
├── styles.css # 样式文件
└── pdf/ # PDF 提词器模块
└── index.html
```
## 🛠️ 技术栈
* HTML5 / CSS3 / JavaScript (ES6+)
* **PeerJS**: 用于实现 WebRTC 远程 P2P 通信。
* **PDF.js**: 用于解析和渲染 PDF 文件。
* **Mammoth.js**: 用于文档处理支持。
## 📄 版权信息
© 2025 SuperJia. All Rights Reserved.

View File

@@ -32,6 +32,10 @@
<button id="pauseScroll">暂停</button>
<button id="resetScroll">重置</button>
</div>
<div class="control-buttons" style="margin-top: 10px;">
<button id="prevLineBtn">上一行</button>
<button id="nextLineBtn">下一行</button>
</div>
</div>
<div class="panel-section">
@@ -79,14 +83,37 @@
</div>
<div class="control-group">
<button id="formatTextBtn" style="width: 100px;">格式化文本</button>
<button id="undoFormatBtn" style="width: 100px; margin-left: 10px;">撤销格式化</button>
<button id="undoFormatBtn" style="width: 100px; margin-left: 10px;">撤销格式化</button>
</div>
<div class="remote-control-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 15px;">
<h4 style="margin-bottom: 10px;">远程协作 (beta)</h4>
<div id="connectionStatus" style="font-size: 12px; color: #666; margin-bottom: 10px;">状态: 未连接</div>
<div class="control-group">
<button id="createRoomBtn" style="width: 100%;">创建房间</button>
</div>
<div id="roomInfo" style="display: none; margin-top: 10px; background: #f5f5f5; padding: 10px; border-radius: 4px;">
<div style="font-size: 12px; margin-bottom: 5px;">房间 ID:</div>
<div style="display: flex; align-items: center; justify-content: space-between;">
<span id="roomIdDisplay" style="font-weight: bold; font-size: 18px; color: #007bff;"></span>
<button id="copyRoomIdBtn" style="padding: 2px 8px; font-size: 12px;">复制</button>
</div>
</div>
<div class="control-group" style="margin-top: 15px;">
<input type="text" id="joinRoomInput" placeholder="输入房间ID" style="flex: 1; padding: 8px;">
<button id="joinRoomBtn" style="width: 80px; margin-left: 10px;">加入</button>
</div>
</div>
</div>
</div>
<div class="panel-section">
<button id="toggleFullscreen">全屏显示</button>
<button id="togglePanel">隐藏控制面板</button>
<button onclick="window.location.href='pdf/index.html'" style="margin-top: 10px; background-color: #6c757d;">前往 PDF 提词器</button>
</div>
<div class="panel-section">
@@ -107,7 +134,7 @@
<!-- 版权信息 -->
<div class="copyright-section">
<p style="margin: 0; padding: 10px 0; text-align: center; font-size: 12px; color: #666; border-top: 1px solid #eee;">
© 2025-Now SuperJia<br> All Rights Reserved<br>版本 v1.2, 发布于2025-09
© 2025-Now SuperJia<br> All Rights Reserved<br>版本 v1.5, 发布于2025-12
</p>
</div>
</div>
@@ -125,6 +152,18 @@
</div>
</div>
<script src="script.js"></script>
<!-- 手机端提示框 -->
<div id="mobileAlert" class="modal-overlay" style="display: none;">
<div class="modal-content">
<h3>温馨提示</h3>
<p>手机屏幕较小,可能不能获得最佳体验。</p>
<p style="font-size: 12px; color: #666; margin-top: 5px;">建议使用平板或电脑访问,或尝试横屏使用。</p>
<button id="closeMobileAlert">我知道了</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.6.0/mammoth.browser.min.js"></script>
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
<script src="script.js" defer></script>
</body>
</html>

View File

@@ -1,3 +1,174 @@
class RemoteController {
constructor(teleprompter) {
this.teleprompter = teleprompter;
this.peer = null;
this.conn = null;
this.roomId = null;
this.isHost = false;
this.isProcessingRemote = false;
this.initializeElements();
this.bindEvents();
}
initializeElements() {
this.createRoomBtn = document.getElementById('createRoomBtn');
this.joinRoomBtn = document.getElementById('joinRoomBtn');
this.joinRoomInput = document.getElementById('joinRoomInput');
this.roomInfo = document.getElementById('roomInfo');
this.roomIdDisplay = document.getElementById('roomIdDisplay');
this.copyRoomIdBtn = document.getElementById('copyRoomIdBtn');
this.connectionStatus = document.getElementById('connectionStatus');
}
bindEvents() {
if(this.createRoomBtn) this.createRoomBtn.addEventListener('click', () => this.createRoom());
if(this.joinRoomBtn) this.joinRoomBtn.addEventListener('click', () => {
const roomId = this.joinRoomInput.value.trim();
if (roomId) this.joinRoom(roomId);
});
if(this.copyRoomIdBtn) this.copyRoomIdBtn.addEventListener('click', () => {
if (this.roomId) {
navigator.clipboard.writeText(this.roomId);
alert('房间 ID 已复制');
}
});
}
generateShortId() {
return Math.floor(1000 + Math.random() * 9000).toString();
}
createRoom() {
const id = this.generateShortId();
this.peer = new Peer(id);
this.peer.on('open', (id) => {
this.roomId = id;
this.isHost = true;
this.showRoomInfo(id);
this.updateStatus('等待连接...');
this.createRoomBtn.disabled = true;
this.joinRoomBtn.disabled = true;
});
this.peer.on('connection', (conn) => {
this.handleConnection(conn);
});
this.peer.on('error', (err) => {
console.error(err);
if (err.type === 'unavailable-id') {
this.peer.destroy();
this.createRoom();
} else {
alert('创建房间失败: ' + err.type);
}
});
}
joinRoom(roomId) {
this.peer = new Peer();
this.peer.on('open', () => {
const conn = this.peer.connect(roomId);
this.handleConnection(conn);
});
this.peer.on('error', (err) => {
console.error(err);
alert('连接失败: ' + err.type);
});
}
handleConnection(conn) {
this.conn = conn;
conn.on('open', () => {
this.updateStatus('已连接');
if (this.isHost) {
this.syncAllState();
}
});
conn.on('data', (data) => {
this.handleData(data);
});
conn.on('close', () => {
this.updateStatus('连接断开');
this.conn = null;
});
}
sendData(type, payload) {
if (this.conn && this.conn.open) {
this.conn.send({ type, payload });
}
}
handleData(data) {
this.isProcessingRemote = true;
try {
switch(data.type) {
case 'fullSync':
this.teleprompter.textInput.value = data.payload.text;
this.teleprompter.formatText(); // 简单显示
// 应用设置
const s = data.payload.settings;
this.teleprompter.scrollSpeed.value = s.scrollSpeed;
this.teleprompter.fontSize.value = s.fontSize;
this.teleprompter.lineHeight.value = s.lineHeight;
this.teleprompter.updateFontSize();
this.teleprompter.updateLineHeight();
if (data.payload.currentLine !== undefined) {
setTimeout(() => {
this.teleprompter.jumpToLineByIndex(data.payload.currentLine);
}, 100);
}
break;
case 'control':
if (data.payload.action === 'start') this.teleprompter.startScrolling();
if (data.payload.action === 'pause') this.teleprompter.pauseScrolling();
if (data.payload.action === 'reset') this.teleprompter.resetScrolling();
if (data.payload.action === 'prevLine') this.teleprompter.prevLine();
if (data.payload.action === 'nextLine') this.teleprompter.nextLine();
break;
case 'text':
this.teleprompter.textInput.value = data.payload.text;
this.teleprompter.formatText();
break;
case 'jump':
this.teleprompter.jumpToLineByIndex(data.payload.index);
break;
}
} finally {
this.isProcessingRemote = false;
}
}
showRoomInfo(id) {
if(this.roomInfo) this.roomInfo.style.display = 'block';
if(this.roomIdDisplay) this.roomIdDisplay.textContent = id;
}
updateStatus(msg) {
if(this.connectionStatus) this.connectionStatus.textContent = '状态: ' + msg;
}
syncAllState() {
this.sendData('fullSync', {
text: this.teleprompter.textInput.value,
settings: {
fontSize: this.teleprompter.fontSize.value,
scrollSpeed: this.teleprompter.scrollSpeed.value,
lineHeight: this.teleprompter.lineHeight.value
}
});
}
}
class Teleprompter {
constructor() {
console.log('开始初始化提词器...');
@@ -15,8 +186,10 @@ class Teleprompter {
this.bindEvents();
this.loadSettings();
this.initializeTextPosition();
this.checkMobileDevice();
console.log('提词器初始化完成');
this.remoteController = new RemoteController(this);
}
initializeElements() {
@@ -32,6 +205,8 @@ class Teleprompter {
this.startScrollBtn = document.getElementById('startScroll');
this.pauseScrollBtn = document.getElementById('pauseScroll');
this.resetScrollBtn = document.getElementById('resetScroll');
this.prevLineBtn = document.getElementById('prevLineBtn');
this.nextLineBtn = document.getElementById('nextLineBtn');
// 显示设置
this.fontSize = document.getElementById('fontSize');
@@ -73,6 +248,10 @@ class Teleprompter {
// 容器
this.container = document.querySelector('.container');
this.teleprompterContainer = document.getElementById('teleprompterContainer');
// 手机端提示
this.mobileAlert = document.getElementById('mobileAlert');
this.closeMobileAlertBtn = document.getElementById('closeMobileAlert');
console.log('元素初始化完成');
}
@@ -92,6 +271,8 @@ class Teleprompter {
this.startScrollBtn.addEventListener('click', () => this.startScrolling());
this.pauseScrollBtn.addEventListener('click', () => this.pauseScrolling());
this.resetScrollBtn.addEventListener('click', () => this.resetScrolling());
if(this.prevLineBtn) this.prevLineBtn.addEventListener('click', () => this.prevLine());
if(this.nextLineBtn) this.nextLineBtn.addEventListener('click', () => this.nextLine());
// 显示设置
this.fontSize.addEventListener('input', () => this.updateFontSize());
@@ -124,6 +305,13 @@ class Teleprompter {
// 全屏状态变化
document.addEventListener('fullscreenchange', () => this.handleFullscreenChange());
// 手机端提示
if (this.closeMobileAlertBtn) {
this.closeMobileAlertBtn.addEventListener('click', () => {
this.mobileAlert.style.display = 'none';
});
}
}
initializeTextPosition() {
@@ -139,6 +327,24 @@ class Teleprompter {
}, 100);
}
checkMobileDevice() {
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768;
console.log('设备检测:', {
userAgent: navigator.userAgent,
width: window.innerWidth,
isMobile: isMobile
});
if (isMobile) {
// 检查是否已经显示过提示
if (!sessionStorage.getItem('mobileAlertShown')) {
this.mobileAlert.style.display = 'flex';
sessionStorage.setItem('mobileAlertShown', 'true');
}
}
}
// 显示文本功能(简单显示,不格式化)
formatText() {
const text = this.textInput.value.trim();
@@ -170,7 +376,9 @@ class Teleprompter {
// 保存到本地存储
localStorage.setItem('teleprompterText', text);
alert('文本显示完成!共' + lines.length + '行');
this.notifyRemote('text', { text: text });
// alert('文本显示完成!共' + lines.length + '行');
}
// 智能格式化功能
@@ -204,7 +412,7 @@ class Teleprompter {
// 保存到本地存储
localStorage.setItem('teleprompterText', text);
alert('智能格式化完成!共' + sentences.length + '行');
// alert('智能格式化完成!共' + sentences.length + '行');
}
// 撤销格式化功能
@@ -238,7 +446,7 @@ class Teleprompter {
// 保存到本地存储
localStorage.setItem('teleprompterText', text);
alert('格式化已撤销!共' + lines.length + '行');
// alert('格式化已撤销!共' + lines.length + '行');
}
splitByPunctuation(text) {
@@ -372,6 +580,7 @@ class Teleprompter {
// 滚动功能
startScrolling() {
this.notifyRemote('control', { action: 'start' });
if (this.formattedText.length === 0) {
alert('请先格式化文本!');
return;
@@ -440,6 +649,7 @@ class Teleprompter {
pauseScrolling() {
if (this.isScrolling && !this.isPaused) {
this.notifyRemote('control', { action: 'pause' });
this.isPaused = true;
clearInterval(this.scrollInterval);
this.pauseScrollBtn.textContent = '继续';
@@ -654,12 +864,40 @@ class Teleprompter {
return;
}
this.jumpToLineByIndex(targetLine - 1);
this.notifyRemote('jump', { index: targetLine - 1 });
}
notifyRemote(type, payload) {
if (this.remoteController && !this.remoteController.isProcessingRemote) {
this.remoteController.sendData(type, payload);
}
}
prevLine() {
if (!this.formattedText || this.formattedText.length === 0) return;
this.jumpToLineByIndex(this.currentLine - 1);
this.notifyRemote('control', { action: 'prevLine' });
}
nextLine() {
if (!this.formattedText || this.formattedText.length === 0) return;
this.jumpToLineByIndex(this.currentLine + 1);
this.notifyRemote('control', { action: 'nextLine' });
}
jumpToLineByIndex(index) {
if (index < 0) index = 0;
if (index >= this.totalLines) index = this.totalLines - 1;
this.stopScrolling();
this.currentLine = targetLine - 1; // 转换为0基索引
this.currentLine = index;
this.currentPosition = this.calculatePositionForLine(this.currentLine);
const isMirror = this.mirrorText.checked;
const mirrorScale = isMirror ? ' scaleX(-1)' : ' scaleX(1)';
this.textDisplay.style.transform = `translateY(-${this.currentPosition}px)${mirrorScale}`;
this.updateProgress();
}

View File

@@ -221,6 +221,43 @@ button {
background: #c82333;
}
#prevLineBtn, #nextLineBtn {
background: #6c757d;
color: white;
flex: 1;
}
#prevLineBtn:hover, #nextLineBtn:hover {
background: #5a6268;
}
#createRoomBtn {
background: #28a745;
color: white;
}
#createRoomBtn:hover {
background: #218838;
}
#joinRoomBtn {
background: #007bff;
color: white;
}
#joinRoomBtn:hover {
background: #0056b3;
}
#copyRoomIdBtn {
background: #e9ecef;
color: #333;
border: 1px solid #ced4da;
padding: 2px 8px;
margin: 0;
}
#copyRoomIdBtn:hover {
background: #dae0e5;
}
#toggleFullscreen {
background: #6f42c1;
color: white;
@@ -469,4 +506,68 @@ body.panel-hidden-bg {
.panel-section:nth-child(2) { animation-delay: 0.2s; }
.panel-section:nth-child(3) { animation-delay: 0.3s; }
.panel-section:nth-child(4) { animation-delay: 0.4s; }
.panel-section:nth-child(5) { animation-delay: 0.5s; }
.panel-section:nth-child(5) { animation-delay: 0.5s; }
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease;
}
.modal-content {
background: white;
padding: 25px;
border-radius: 12px;
width: 85%;
max-width: 320px;
text-align: center;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
transform: translateY(0);
animation: slideUp 0.3s ease;
}
.modal-content h3 {
color: #333;
margin-bottom: 15px;
font-size: 18px;
}
.modal-content p {
color: #555;
margin-bottom: 10px;
line-height: 1.5;
}
.modal-content button {
margin-top: 15px;
width: 100%;
background: #667eea;
color: white;
padding: 12px;
font-size: 15px;
}
.modal-content button:hover {
background: #5a6fd8;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}