main
土狗 2 months ago
parent c5da12cb61
commit d57a3c9225

@ -0,0 +1,3 @@
ELECTRON_BUILDER_BINARIES_MIRROR=https://mirrors.huaweicloud.com/electron-builder-binaries/
registry=https://registry.npm.taobao.org/
ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron-builder-binaries/

@ -1,2 +1,108 @@
# ez4prompt
a teleprompter.
# 🎬 智能提词器 (ez4prompt)
![Logo](readme.src/icon.png)
一个现代化的智能提词器应用,专为主播、演讲者和内容创作者设计!✨
## 🌟 特色功能
### 📝 智能文本编辑
- 🖊️ 实时文本编辑,支持直接输入或粘贴内容
- 📁 支持导入 `.txt` 文本文件
- 🔒 文本锁定功能,防止误操作
### 🎯 精准滚动控制
- ⚡ 智能自动滚动,可按行或按字滚动
- 🎛️ 可调节滚动速度1-100级精细控制
- ⏱️ 根据全文时长自动计算滚动速度
- ⏰ 倒计时功能,精确控制开始时机
- 📍 红色定位线,清晰标示当前阅读位置
### 🎨 个性化定制
- 🌙 深色/浅色主题切换
- 🔤 多种字体选择(宋体、微软雅黑、黑体、楷体)
- 📏 字号、颜色、间距全面可调
- 📐 左右边距、行距自由设置
### 🔄 灵活显示选项
- 🔀 水平/垂直翻转功能
- 💧 自定义水印设置
- 🖥️ 全屏模式支持
- 📱 响应式设计,支持移动端
### 🎙️ 录音功能
- 🎵 内置录音功能
- 🎧 录音试听
- 💾 录音保存
- 🔄 音频智能识别滚动(实验性功能)
## 🚀 快速开始
### 在线使用
直接访问部署的网站即可开始使用!
### 本地运行
```bash
# 克隆项目
git clone https://github.com/Song2770/ez4prompt.git
# 进入项目目录
cd ez4prompt
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 构建生产版本
npm run build
```
## 🎯 使用指南
### 基础操作
1. **📝 输入文本**:直接在编辑区输入内容,或点击导入按钮上传文本文件
2. **⚙️ 调整设置**:点击左侧功能按钮,设置字体、滚动速度等参数
3. **▶️ 开始播放**:点击播放按钮开始自动滚动
4. **⏸️ 暂停控制**:随时点击暂停按钮停止滚动
### 高级功能
- **🔒 锁定文本**:防止在演示过程中误编辑文本
- **🔄 翻转显示**:适配不同的显示设备和场景
- **💧 添加水印**:为内容添加个性化标识
- **🎙️ 录音同步**:录制音频的同时进行提词
## 🛠️ 技术栈
- **前端框架**:原生 HTML5 + CSS3 + JavaScript
- **构建工具**Vite
- **样式方案**CSS Modules + Tailwind CSS
- **设计风格**Google Material Design
## 🎨 设计理念
- **🎯 用户体验优先**:简洁直观的界面设计
- **⚡ 性能优化**:轻量级实现,流畅运行
- **📱 响应式设计**:完美适配各种设备
- **🎨 现代化界面**Material Design 风格,高端低调
## 🤝 贡献指南
欢迎提交 Issue 和 Pull Request
如果你觉得这个项目对你有帮助,请给我们一个 ⭐ Star
## 📄 开源协议
本项目采用 GPLv3.0 协议开源。
## 📞 联系我们
- 🐛 **问题反馈**[GitHub Issues](https://github.com/Song2770/ez4prompt/issues)
- 💡 **功能建议**:欢迎在 Issues 中提出
- 📧 **其他联系**:通过 GitHub 联系项目维护者
---
💡 **小贴士**:首次使用建议先设置好字体大小和滚动速度,然后就可以享受流畅的提词体验啦!🎉

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" fill="currentColor"/>
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 180 B

After

Width:  |  Height:  |  Size: 181 B

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M9 4v3h5v12h3V7h5V4H9zm-6 8h3v7h3v-7h3V9H3v3z" fill="currentColor"/>
<path d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 180 B

After

Width:  |  Height:  |  Size: 185 B

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" fill="currentColor"/>
<path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 172 B

After

Width:  |  Height:  |  Size: 228 B

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z" fill="currentColor"/>
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM12 17c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1s3.1 1.39 3.1 3.1v2z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 360 B

After

Width:  |  Height:  |  Size: 354 B

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" fill="currentColor"/>
<path d="M10 2c-1.82 0-3.53.5-5 1.35C7.99 5.08 10 8.3 10 12s-2.01 6.92-5 8.65C6.47 21.5 8.18 22 10 22c5.52 0 10-4.48 10-10S15.52 2 10 2z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 182 B

After

Width:  |  Height:  |  Size: 262 B

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z" fill="currentColor"/>
<path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 190 B

After

Width:  |  Height:  |  Size: 179 B

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z" fill="currentColor"/>
<path d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79 1.42-1.41zM4 10.5H1v2h3v-2zm9-9.95h-2V3.5h2V.55zm7.45 3.91l-1.41-1.41-1.79 1.79 1.41 1.41 1.79-1.79zm-3.21 13.7l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM20 10.5v2h3v-2h-3zm-8-5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm-1 16.95h2V19.5h-2v2.95zm-7.45-3.91l1.41 1.41 1.79-1.8-1.41-1.41-1.79 1.8z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 929 B

After

Width:  |  Height:  |  Size: 476 B

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 18.5c-3.04 0-5.5-2.46-5.5-5.5s2.46-5.5 5.5-5.5S17.5 9.96 17.5 13s-2.46 5.5-5.5 5.5zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18V4c4.41 0 8 3.59 8 8s-3.59 8-8 8z" fill="currentColor"/>
<path d="M20 15.31L23.31 12 20 8.69V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69zM12 18V6c3.31 0 6 2.69 6 6s-2.69 6-6 6z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 280 B

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6h1.9c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" fill="currentColor"/>
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6h1.9c0-1.71 1.39-3.1 3.1-3.1s3.1 1.39 3.1 3.1v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM12 17c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 349 B

After

Width:  |  Height:  |  Size: 343 B

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="currentColor"/>
<path d="M23 12l-2.44-2.78.34-3.68-3.61-.82-1.89-3.18L12 3 8.6 1.54 6.71 4.72l-3.61.81.34 3.68L1 12l2.44 2.78-.34 3.69 3.61.82 1.89 3.18L12 21l3.4 1.46 1.89-3.18 3.61-.82-.34-3.68L23 12zm-13 5l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9l-8 8z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 363 B

@ -8,16 +8,17 @@
<link rel="stylesheet" href="styles/theme.css">
<link rel="stylesheet" href="styles/sidebar.css">
<link rel="stylesheet" href="styles/editor.css">
<link rel="stylesheet" href="styles/audio-player.css">
</head>
<body class="dark-theme">
<!-- 顶部信息栏 -->
<header class="top-header">
<div class="logo-area">
<!-- Logo区域 - 居中圆角控件 -->
<div class="logo-widget">
<img src="images/logo.svg" alt="Logo" class="logo">
<span class="project-name">智能提词器</span>
</div>
<div class="time-display" id="timeDisplay"></div>
</header>
<!-- 计时器 - 右上角圆角控件 -->
<div class="time-widget" id="timeDisplay"></div>
<!-- 红色定位线 -->
<div class="reading-line" id="readingLine"></div>
@ -64,14 +65,17 @@
<img src="images/record.svg" alt="录音">
</button>
<button class="sidebar-btn" id="languageBtn" title="切换语言">
<!-- <button class="sidebar-btn" id="languageBtn" title="切换语言">
<img src="images/language.svg" alt="语言">
</button>
</button> -->
</aside>
<!-- 侧边栏切换按钮 - 独立于侧边栏右侧 -->
<div class="sidebar-toggle-container" id="sidebarToggleContainer">
<button class="sidebar-btn sidebar-toggle" id="sidebarToggle" title="隐藏功能区">
<img src="images/chevron-left.svg" alt="隐藏">
</button>
</aside>
</div>
<!-- 左侧边缘触发区域 -->
<div class="sidebar-trigger" id="sidebarTrigger"></div>
@ -96,10 +100,10 @@
<h3>滚动设置</h3>
<div class="setting-group">
<label>
<input type="checkbox" id="autoScroll"> 自动滚动
<input type="checkbox" id="autoScroll" checked> 自动滚动
</label>
</div>
<div class="setting-group" id="autoScrollOptions" style="display: none;">
<div class="setting-group" id="autoScrollOptions">
<label>滚动类型:
<select id="scrollType">
<option value="line">按行滚动</option>
@ -107,8 +111,8 @@
</select>
</label>
<label>滚动速度:
<input type="range" id="scrollSpeed" min="1" max="100" value="30">
<span id="scrollSpeedValue">30</span>
<input type="range" id="scrollSpeed" min="1" max="100" value="10">
<span id="scrollSpeedValue">10</span>
</label>
<label>
<input type="checkbox" id="autoDuration"> 根据全文时长自动计算
@ -118,6 +122,9 @@
<input type="number" id="totalDuration" min="1" max="120" value="10">
</label>
</div>
<label>倒计时时长(秒):
<input type="number" id="countdownDuration" min="0" max="10" value="0">
</label>
</div>
</div>
@ -133,15 +140,15 @@
</select>
</label>
<label>字号:
<input type="range" id="fontSize" min="12" max="72" value="16">
<span id="fontSizeValue">16px</span>
<input type="range" id="fontSize" min="12" max="100" value="40">
<span id="fontSizeValue">40px</span>
</label>
<label>颜色:
<input type="color" id="fontColor" value="#333333">
</label>
<label>左边距:
<input type="range" id="marginLeft" min="0" max="200" value="50">
<span id="marginLeftValue">50px</span>
<input type="range" id="marginLeft" min="0" max="200" value="100">
<span id="marginLeftValue">100px</span>
</label>
<label>右边距:
<input type="range" id="marginRight" min="0" max="200" value="50">
@ -196,35 +203,45 @@
<button id="startRecord" class="record-control-btn">开始录音</button>
<button id="stopRecord" class="record-control-btn" disabled>停止录音</button>
<button id="playRecord" class="record-control-btn" disabled>试听</button>
<button id="saveRecord" class="record-control-btn" disabled>保存录音</button>
<label>
<input type="checkbox" id="audioSync"> 音频智能识别滚动
</label>
</div>
</div>
<!-- 倒计时设置面板 -->
<div class="settings-panel" id="countdownSettings" style="display: none;">
<h3>倒计时设置</h3>
<div class="setting-group">
<label>倒计时时长(秒):
<input type="number" id="countdownDuration" min="0" max="10" value="0">
</label>
<button id="startCountdown" class="record-control-btn">开始倒计时</button>
</div>
</div>
<!-- 倒计时显示 -->
<div class="countdown-display" id="countdownDisplay" style="display: none;">
<span id="countdownText">3</span>
</div>
<!-- 音频播放弹窗 -->
<div class="audio-player-modal" id="audioPlayerModal" style="display: none;">
<div class="audio-player-content">
<div class="audio-player-header">
<h3>录音试听</h3>
<button class="close-btn" id="closeAudioPlayer">×</button>
</div>
<div class="audio-player-body">
<audio id="audioPlayer" controls>
您的浏览器不支持音频播放。
</audio>
<div class="audio-info">
<span id="audioFileName">录音文件</span>
<span id="audioDuration">--:--</span>
</div>
</div>
<div class="audio-player-footer">
<button id="replayAudio" class="audio-control-btn">重新播放</button>
<button id="downloadAudio" class="audio-control-btn">下载录音</button>
</div>
</div>
</div>
<!-- 隐藏的文件输入 -->
<input type="file" id="fileInput" accept=".txt" style="display: none;">
<!-- 音频元素 -->
<audio id="audioPlayer" style="display: none;"></audio>
<script src="js/main.js"></script>
<script src="js/theme.js"></script>
<script src="js/scroll.js"></script>

@ -20,7 +20,6 @@ class AudioController {
const startBtn = document.getElementById('startRecord');
const stopBtn = document.getElementById('stopRecord');
const playBtn = document.getElementById('playRecord');
const saveBtn = document.getElementById('saveRecord');
const audioSyncCheckbox = document.getElementById('audioSync');
startBtn.addEventListener('click', () => {
@ -35,13 +34,12 @@ class AudioController {
this.playRecording();
});
saveBtn.addEventListener('click', () => {
this.saveRecording();
});
audioSyncCheckbox.addEventListener('change', () => {
this.toggleAudioSync(audioSyncCheckbox.checked);
});
// 音频播放弹窗控制
this.setupAudioPlayerModal();
}
async startRecording() {
@ -90,36 +88,21 @@ class AudioController {
playRecording() {
if (this.audioUrl) {
const audio = document.getElementById('audioPlayer');
audio.src = this.audioUrl;
audio.play();
this.showAudioPlayerModal();
}
}
saveRecording() {
if (this.audioBlob) {
const url = URL.createObjectURL(this.audioBlob);
const a = document.createElement('a');
a.href = url;
a.download = `teleprompter-recording-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.wav`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}
updateRecordingControls() {
const startBtn = document.getElementById('startRecord');
const stopBtn = document.getElementById('stopRecord');
const playBtn = document.getElementById('playRecord');
const saveBtn = document.getElementById('saveRecord');
if (this.isRecording) {
startBtn.disabled = true;
stopBtn.disabled = false;
playBtn.disabled = true;
saveBtn.disabled = true;
startBtn.textContent = '录音中...';
startBtn.style.background = '#dc3545';
} else {
@ -130,7 +113,6 @@ class AudioController {
if (this.audioUrl) {
playBtn.disabled = false;
saveBtn.disabled = false;
}
}
}
@ -197,9 +179,107 @@ class AudioController {
}
toggleAudioSync(enabled) {
if (enabled && !this.recognition) {
alert('当前浏览器不支持语音识别功能');
document.getElementById('audioSync').checked = false;
if (enabled && this.recognition) {
console.log('音频智能识别滚动已启用');
} else {
console.log('音频智能识别滚动已禁用');
}
}
setupAudioPlayerModal() {
const modal = document.getElementById('audioPlayerModal');
const closeBtn = document.getElementById('closeAudioPlayer');
const replayBtn = document.getElementById('replayAudio');
const downloadBtn = document.getElementById('downloadAudio');
const audio = document.getElementById('audioPlayer');
// 关闭弹窗
closeBtn.addEventListener('click', () => {
this.hideAudioPlayerModal();
});
// 点击背景关闭弹窗
modal.addEventListener('click', (e) => {
if (e.target === modal) {
this.hideAudioPlayerModal();
}
});
// 重新播放
replayBtn.addEventListener('click', () => {
audio.currentTime = 0;
audio.play();
});
// 下载录音
downloadBtn.addEventListener('click', () => {
if (this.audioBlob) {
const url = URL.createObjectURL(this.audioBlob);
const a = document.createElement('a');
a.href = url;
a.download = `teleprompter-recording-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.wav`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
});
// 音频加载完成时更新时长
audio.addEventListener('loadedmetadata', () => {
this.updateAudioInfo();
});
// ESC键关闭弹窗
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.style.display === 'block') {
this.hideAudioPlayerModal();
}
});
}
showAudioPlayerModal() {
const modal = document.getElementById('audioPlayerModal');
const audio = document.getElementById('audioPlayer');
// 设置音频源
audio.src = this.audioUrl;
// 显示弹窗
modal.style.display = 'flex';
// 更新音频信息
this.updateAudioInfo();
}
hideAudioPlayerModal() {
const modal = document.getElementById('audioPlayerModal');
const audio = document.getElementById('audioPlayer');
// 暂停播放
audio.pause();
// 隐藏弹窗
modal.style.display = 'none';
}
updateAudioInfo() {
const audio = document.getElementById('audioPlayer');
const fileNameSpan = document.getElementById('audioFileName');
const durationSpan = document.getElementById('audioDuration');
// 更新文件名
const now = new Date();
const fileName = `录音-${now.toLocaleDateString()}-${now.toLocaleTimeString()}`;
fileNameSpan.textContent = fileName;
// 更新时长
if (audio.duration && !isNaN(audio.duration)) {
const minutes = Math.floor(audio.duration / 60);
const seconds = Math.floor(audio.duration % 60);
durationSpan.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
} else {
durationSpan.textContent = '--:--';
}
}
}

@ -6,7 +6,6 @@ class TeleprompterApp {
this.currentSettings = this.getDefaultSettings();
this.scrollController = null;
this.activePanel = null;
this.currentLanguage = 'zh-CN';
this.sidebarHidden = false;
this.countdownDuration = 0;
@ -36,22 +35,21 @@ class TeleprompterApp {
getDefaultSettings() {
return {
theme: 'dark',
fontSize: 16,
fontSize: 40,
fontFamily: 'SimSun, serif',
fontColor: '#ffffff',
marginLeft: 50,
marginLeft: 100,
marginRight: 50,
letterSpacing: 1,
lineHeight: 1.6,
autoScroll: false,
autoScroll: true,
scrollType: 'line',
scrollSpeed: 30,
scrollSpeed: 10,
horizontalFlip: false,
verticalFlip: false,
watermarkEnabled: false,
watermarkText: '',
watermarkType: 'center',
language: 'zh-CN',
countdownDuration: 0
};
}
@ -67,14 +65,19 @@ class TeleprompterApp {
window.themeController.toggleTheme();
});
// 播放/暂停 - 现在包含倒计时功能
// 播放/暂停 - 检查自动滚动设置
document.getElementById('playBtn').addEventListener('click', () => {
this.showCountdownSettings();
});
const autoScrollEnabled = document.getElementById('autoScroll').checked;
if (!autoScrollEnabled) {
alert('请先在滚动设置中启用自动滚动功能');
return;
}
// 语言切换
document.getElementById('languageBtn').addEventListener('click', () => {
this.toggleLanguage();
if (this.isPlaying) {
this.pauseReading();
} else {
this.startCountdownAndPlay();
}
});
// 文本锁定
@ -99,10 +102,7 @@ class TeleprompterApp {
window.textController.handleFileImport(e);
});
// 倒计时设置
document.getElementById('startCountdown').addEventListener('click', () => {
this.startCountdownAndPlay();
});
// 键盘快捷键
document.addEventListener('keydown', (e) => {
@ -146,6 +146,9 @@ class TeleprompterApp {
const panelId = this.getPanelIdForButton(btn.id);
if (panelId) {
this.showSettingsPanel(panelId, btn);
} else {
// 如果当前按钮没有对应的设置面板,关闭已打开的面板
this.closeAllPanels();
}
}
});
@ -196,29 +199,31 @@ class TeleprompterApp {
// 定位面板
const rect = buttonElement.getBoundingClientRect();
panel.style.left = `${rect.right + 10}px`;
panel.style.top = `${rect.top}px`;
let leftPos = rect.right + 10;
let topPos = rect.top;
// 激活按钮样式
buttonElement.classList.add('active');
// 检查面板是否会超出屏幕底部
const panelHeight = panel.offsetHeight || 200; // 默认高度
const windowHeight = window.innerHeight;
if (topPos + panelHeight > windowHeight) {
// 如果超出底部,将面板向上调整
topPos = windowHeight - panelHeight - 20; // 留20px边距
}
showCountdownSettings() {
this.closeAllPanels();
// 确保面板不会超出屏幕顶部
if (topPos < 20) {
topPos = 20;
}
const panel = document.getElementById('countdownSettings');
const playBtn = document.getElementById('playBtn');
panel.style.left = `${leftPos}px`;
panel.style.top = `${topPos}px`;
panel.style.display = 'block';
this.activePanel = 'countdownSettings';
// 激活按钮样式
buttonElement.classList.add('active');
}
// 定位面板
const rect = playBtn.getBoundingClientRect();
panel.style.left = `${rect.right + 10}px`;
panel.style.top = `${rect.top}px`;
playBtn.classList.add('active');
}
toggleSidebar() {
const sidebar = document.getElementById('sidebar');
@ -285,50 +290,6 @@ class TeleprompterApp {
}
}
toggleLanguage() {
// 简单的语言切换示例
if (this.currentLanguage === 'zh-CN') {
this.currentLanguage = 'en-US';
this.updateLanguageDisplay('English');
} else {
this.currentLanguage = 'zh-CN';
this.updateLanguageDisplay('中文');
}
// 保存语言设置
localStorage.setItem('teleprompter-language', this.currentLanguage);
// 更新语音识别语言
if (window.audioController && window.audioController.recognition) {
window.audioController.recognition.lang = this.currentLanguage;
}
}
updateLanguageDisplay(languageName) {
// 可以在这里更新界面语言显示
console.log(`语言已切换到: ${languageName}`);
// 显示临时提示
const notification = document.createElement('div');
notification.textContent = `语言已切换到: ${languageName}`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 20px;
border-radius: 6px;
z-index: 2000;
animation: fadeInOut 2s ease;
`;
document.body.appendChild(notification);
setTimeout(() => {
document.body.removeChild(notification);
}, 2000);
}
startCountdownAndPlay() {
this.countdownDuration = parseInt(document.getElementById('countdownDuration').value) || 0;
this.closeAllPanels();
@ -498,12 +459,19 @@ class TeleprompterApp {
// 拖拽滚动条
let isDragging = false;
let wasAutoScrolling = false;
thumb.addEventListener('mousedown', (e) => {
isDragging = true;
const startY = e.clientY;
const startScrollTop = editor.scrollTop;
// 如果正在自动滚动,暂停自动滚动
if (window.scrollController && window.scrollController.isAutoScrolling) {
wasAutoScrolling = true;
window.scrollController.stopAutoScroll();
}
const mouseMoveHandler = (e) => {
if (!isDragging) return;
@ -516,6 +484,14 @@ class TeleprompterApp {
const mouseUpHandler = () => {
isDragging = false;
// 如果之前在自动滚动,更新滚动控制器的当前位置并恢复自动滚动
if (wasAutoScrolling && window.scrollController) {
window.scrollController.currentScrollPosition = editor.scrollTop;
window.scrollController.startAutoScroll();
}
wasAutoScrolling = false;
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};
@ -531,11 +507,33 @@ class TeleprompterApp {
const clickRatio = e.offsetY / scrollbar.clientHeight;
const newScrollTop = clickRatio * (editor.scrollHeight - editor.clientHeight);
// 如果正在自动滚动,暂停并更新位置
const wasAutoScrollingOnClick = window.scrollController && window.scrollController.isAutoScrolling;
if (wasAutoScrollingOnClick) {
window.scrollController.stopAutoScroll();
}
// 使用平滑滚动
if (window.scrollController) {
window.scrollController.smoothScrollTo(newScrollTop);
// 如果之前在自动滚动,在平滑滚动完成后恢复自动滚动
if (wasAutoScrollingOnClick) {
setTimeout(() => {
if (window.scrollController) {
window.scrollController.currentScrollPosition = newScrollTop;
window.scrollController.startAutoScroll();
}
}, 500); // 等待平滑滚动完成500ms
}
} else {
editor.scrollTop = newScrollTop;
// 如果之前在自动滚动,立即恢复
if (wasAutoScrollingOnClick && window.scrollController) {
window.scrollController.currentScrollPosition = newScrollTop;
window.scrollController.startAutoScroll();
}
}
});

@ -119,15 +119,15 @@ class SettingsController {
applyDefaultSettings() {
const defaultSettings = {
fontFamily: 'SimSun, serif',
fontSize: 16,
fontSize: 40,
fontColor: window.themeController?.isDarkTheme() ? '#ffffff' : '#333333',
marginLeft: 50,
marginLeft: 100,
marginRight: 50,
letterSpacing: 1,
lineHeight: 1.6,
autoScroll: false,
autoScroll: true,
scrollType: 'line',
scrollSpeed: 30,
scrollSpeed: 10,
horizontalFlip: false,
verticalFlip: false,
watermarkEnabled: false,

@ -0,0 +1,223 @@
/* 音频播放弹窗样式 */
.audio-player-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.audio-player-content {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 0;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
min-width: 400px;
max-width: 500px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.audio-player-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.audio-player-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
color: #666;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.close-btn:hover {
background: rgba(0, 0, 0, 0.1);
color: #333;
}
.audio-player-body {
padding: 24px;
}
.audio-player-body audio {
width: 100%;
margin-bottom: 16px;
border-radius: 8px;
outline: none;
}
.audio-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
color: #666;
margin-bottom: 16px;
}
#audioFileName {
font-weight: 500;
color: #333;
}
#audioDuration {
font-family: 'Courier New', monospace;
background: rgba(0, 0, 0, 0.05);
padding: 4px 8px;
border-radius: 4px;
}
.audio-player-footer {
padding: 16px 24px 24px;
display: flex;
gap: 12px;
justify-content: flex-end;
}
.audio-control-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
background: #f5f5f5;
color: #333;
}
.audio-control-btn:hover {
background: #e0e0e0;
transform: translateY(-1px);
}
.audio-control-btn:active {
transform: translateY(0);
}
#downloadAudio {
background: #2196F3;
color: white;
}
#downloadAudio:hover {
background: #1976D2;
}
/* 深色主题适配 */
.dark-theme .audio-player-content {
background: rgba(40, 40, 40, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.dark-theme .audio-player-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.dark-theme .audio-player-header h3 {
color: #ffffff;
}
.dark-theme .close-btn {
color: #cccccc;
}
.dark-theme .close-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
.dark-theme .audio-info {
color: #cccccc;
}
.dark-theme #audioFileName {
color: #ffffff;
}
.dark-theme #audioDuration {
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
.dark-theme .audio-control-btn {
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
.dark-theme .audio-control-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.dark-theme #downloadAudio {
background: #2196F3;
color: white;
}
.dark-theme #downloadAudio:hover {
background: #1976D2;
}
/* 移动端适配 */
@media (max-width: 768px) {
.audio-player-content {
min-width: 90vw;
max-width: 90vw;
margin: 20px;
}
.audio-player-footer {
flex-direction: column;
}
.audio-control-btn {
width: 100%;
}
}

@ -69,7 +69,8 @@
/* 移动端文本编辑器 */
@media (max-width: 768px) {
.text-editor {
padding: 15px 30px;
padding: 15px 60px;
padding-bottom: 75vh; /* 移动端也保持底部额外空间 */
font-size: 14px;
line-height: 1.5;
}

@ -13,43 +13,65 @@ body {
transition: all 0.3s ease;
}
/* 顶部信息栏 */
.top-header {
/* Logo控件 - 居中显示 */
.logo-widget {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
z-index: 1000;
gap: 10px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 8px 16px;
z-index: 1000;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
}
.logo-area {
display: flex;
align-items: center;
gap: 10px;
.logo-widget:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateX(-50%) translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
.logo {
width: 32px;
height: 32px;
width: 24px;
height: 24px;
}
.project-name {
font-size: 18px;
font-size: 16px;
font-weight: bold;
white-space: nowrap;
}
.time-display {
font-size: 16px;
/* 计时器控件 - 右上角显示 */
.time-widget {
position: fixed;
top: 20px;
right: 20px;
font-size: 14px;
font-family: 'Courier New', monospace;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 8px 12px;
z-index: 1000;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
min-width: 80px;
text-align: right;
text-align: center;
}
.time-widget:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
/* 红色定位线 */
@ -58,17 +80,17 @@ body {
top: 25%;
left: 0;
right: 0;
height: 3px;
background: #ff4444;
height: 4px;
background: linear-gradient(to right, transparent 0%, #ff4444 10%, #ff4444 95%, transparent 100%);
z-index: 500;
box-shadow: 0 0 10px rgba(255, 68, 68, 0.5);
box-shadow: 0 0 40px rgba(255, 68, 68, 0.9);
transition: opacity 0.3s ease;
}
/* 主内容区域 */
.main-content {
height: 100vh;
padding-top: 60px;
padding-top: 0;
position: relative;
overflow: hidden;
}
@ -81,7 +103,10 @@ body {
.text-editor {
flex: 1;
padding: 20px 50px;
padding-top: 12vh;
padding-bottom: 75vh; /* 底部额外空间相当于屏幕3/4高度 */
padding-left: 40px;
padding-right: 40px;
font-size: 16px;
line-height: 1.6;
letter-spacing: 1px;
@ -160,6 +185,7 @@ body {
gap: 8px;
font-size: 14px;
color: #555;
padding-bottom: 5px;
}
.setting-group input[type="range"] {
@ -217,6 +243,7 @@ body {
background: rgba(0, 0, 0, 0.8);
color: white;
width: 100px;
backdrop-filter: blur(10px);
height: 100px;
border-radius: 50%;
display: flex;
@ -254,7 +281,7 @@ body {
}
.text-editor {
padding: 15px 20px;
padding: 15px 40px;
font-size: 14px;
}
}

@ -74,11 +74,25 @@
border: 2px solid #007bff;
}
/* 侧边栏切换按钮容器 */
.sidebar-toggle-container {
position: fixed;
left: 90px; /* 侧边栏右侧 */
top: 50%;
transform: translateY(-50%);
z-index: 1001;
transition: all 0.3s ease;
}
/* 当侧边栏隐藏时,切换按钮贴边 */
.sidebar.hidden ~ .sidebar-toggle-container {
left: 10px; /* 贴边位置 */
}
/* 隐藏/显示按钮特殊样式 */
.sidebar-toggle {
margin-top: 10px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
padding-top: 10px;
transition: all 0.3s ease;
width: 25px;
}
/* 左侧边缘触发区域 */
@ -144,6 +158,14 @@
.sidebar-trigger {
width: 15px;
}
.sidebar-toggle-container {
left: 70px; /* 移动端侧边栏右侧 */
}
.sidebar.hidden ~ .sidebar-toggle-container {
left: 10px; /* 移动端贴边位置 */
}
}
/* 滚动时自动隐藏 */

@ -4,9 +4,16 @@
color: #333333;
}
.light-theme .top-header {
background: rgba(238, 247, 252, 0.9);
.light-theme .logo-widget {
background: rgba(255, 255, 255, 0.9);
color: #333333;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.light-theme .time-widget {
background: rgba(255, 255, 255, 0.9);
color: #333333;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.light-theme .lock-btn {
@ -51,9 +58,16 @@
color: #ffffff;
}
.dark-theme .top-header {
background: rgba(20, 20, 20, 0.9);
.dark-theme .logo-widget {
background: rgba(40, 40, 40, 0.9);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.dark-theme .time-widget {
background: rgba(40, 40, 40, 0.9);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.dark-theme .lock-btn {
@ -103,6 +117,10 @@
border: 1px solid rgba(255, 255, 255, 0.1);
}
.dark-theme .sidebar-btn img {
filter: invert(1) brightness(0.9);
}
.dark-theme .record-control-btn {
background: #007bff;
}

Loading…
Cancel
Save