first commit

main
土狗 2 months ago
parent 9b125f5714
commit c5da12cb61

@ -1,2 +0,0 @@
# ez4prompt
一个提词器。

@ -0,0 +1,28 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
);

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 180 B

@ -0,0 +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"/>
</svg>

After

Width:  |  Height:  |  Size: 180 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M15 21h2v-2h-2v2zm4-12h2V7h-2v2zM3 5v14c0 1.1.9 2 2 2h4v-2H5V5h4V3H5c-1.1 0-2 .9-2 2zm16-2v2h2c0-1.1-.9-2-2-2zm-8 2h2V3h-2v2zm8 4h2v-2h-2v2zm0 4h2v-2h-2v2zm-8 8h2v-2h-2v2zm4 0h2v-2h-2v2zm4-8h2v-2h-2v2zm0 4h2v-2h-2v2z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 351 B

@ -0,0 +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"/>
</svg>

After

Width:  |  Height:  |  Size: 180 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 213 B

@ -0,0 +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"/>
</svg>

After

Width:  |  Height:  |  Size: 172 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-.32-1.25-.78-2.45-1.38-3.56 1.84.63 3.37 1.91 4.33 3.56zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56-1.84-.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95c-.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 944 B

@ -0,0 +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"/>
</svg>

After

Width:  |  Height:  |  Size: 360 B

@ -0,0 +1,20 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="144.000000pt" height="144.000000pt" viewBox="0 0 144.000000 144.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,144.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M329 1287 c-91 -26 -160 -99 -179 -193 -8 -36 -10 -171 -8 -406 3
-307 6 -358 21 -390 25 -55 71 -103 125 -129 l47 -24 389 3 c387 3 390 4 436
27 52 26 92 68 118 124 15 32 17 78 17 421 0 424 1 419 -64 493 -18 19 -53 46
-79 59 -46 23 -53 23 -412 25 -268 2 -377 -1 -411 -10z m587 -221 l-36 -44 0
-346 0 -346 -275 0 -275 0 0 390 0 390 311 0 311 0 -36 -44z m152 -10 l42 -53
0 -237 0 -236 -90 0 -90 0 0 236 0 236 42 54 c24 30 45 54 48 54 3 -1 24 -25
48 -54z m40 -649 l3 -77 -91 0 -90 0 0 75 0 74 73 4 c39 2 79 3 87 2 12 0 16
-17 18 -78z"/>
<path d="M472 863 l3 -68 148 -3 147 -3 0 71 0 70 -150 0 -151 0 3 -67z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -0,0 +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"/>
</svg>

After

Width:  |  Height:  |  Size: 182 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 166 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M8 5v14l11-7z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 148 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="8" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 154 B

@ -0,0 +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"/>
</svg>

After

Width:  |  Height:  |  Size: 190 B

@ -0,0 +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"/>
</svg>

After

Width:  |  Height:  |  Size: 929 B

@ -0,0 +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"/>
</svg>

After

Width:  |  Height:  |  Size: 326 B

@ -0,0 +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"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

@ -0,0 +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"/>
</svg>

After

Width:  |  Height:  |  Size: 252 B

@ -0,0 +1,237 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能提词器</title>
<link rel="stylesheet" href="styles/main.css">
<link rel="stylesheet" href="styles/theme.css">
<link rel="stylesheet" href="styles/sidebar.css">
<link rel="stylesheet" href="styles/editor.css">
</head>
<body class="dark-theme">
<!-- 顶部信息栏 -->
<header class="top-header">
<div class="logo-area">
<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="reading-line" id="readingLine"></div>
<!-- 左侧悬浮工具栏 -->
<aside class="sidebar" id="sidebar">
<button class="sidebar-btn" id="fullscreenBtn" title="全屏">
<img src="images/fullscreen.svg" alt="全屏">
</button>
<button class="sidebar-btn" id="themeBtn" title="主题切换">
<img src="images/theme.svg" alt="主题">
</button>
<button class="sidebar-btn" id="playBtn" title="播放/暂停">
<img src="images/play.svg" alt="播放">
</button>
<button class="sidebar-btn" id="scrollBtn" title="滚动设置">
<img src="images/scroll.svg" alt="滚动">
</button>
<button class="sidebar-btn" id="fontBtn" title="字体设置">
<img src="images/font.svg" alt="字体">
</button>
<button class="sidebar-btn" id="flipBtn" title="翻转设置">
<img src="images/flip.svg" alt="翻转">
</button>
<button class="sidebar-btn" id="watermarkBtn" title="水印设置">
<img src="images/watermark.svg" alt="水印">
</button>
<button class="sidebar-btn" id="importBtn" title="导入文件">
<img src="images/import.svg" alt="导入">
</button>
<button class="sidebar-btn" id="lockBtn" title="锁定文本">
<img src="images/unlock.svg" alt="锁定" id="lockIcon">
</button>
<button class="sidebar-btn" id="recordBtn" title="录音">
<img src="images/record.svg" alt="录音">
</button>
<button class="sidebar-btn" id="languageBtn" title="切换语言">
<img src="images/language.svg" alt="语言">
</button>
<button class="sidebar-btn sidebar-toggle" id="sidebarToggle" title="隐藏功能区">
<img src="images/chevron-left.svg" alt="隐藏">
</button>
</aside>
<!-- 左侧边缘触发区域 -->
<div class="sidebar-trigger" id="sidebarTrigger"></div>
<!-- 主文本编辑区域 -->
<main class="main-content">
<div class="text-editor-container">
<div class="text-editor" id="textEditor" contenteditable="true"></div>
</div>
<!-- 水印层 -->
<div class="watermark-layer" id="watermarkLayer"></div>
</main>
<!-- 右侧滚动条 -->
<div class="custom-scrollbar" id="customScrollbar">
<div class="scrollbar-thumb" id="scrollbarThumb"></div>
</div>
<!-- 设置面板模板 -->
<div class="settings-panel" id="scrollSettings" style="display: none;">
<h3>滚动设置</h3>
<div class="setting-group">
<label>
<input type="checkbox" id="autoScroll"> 自动滚动
</label>
</div>
<div class="setting-group" id="autoScrollOptions" style="display: none;">
<label>滚动类型:
<select id="scrollType">
<option value="line">按行滚动</option>
<option value="char">按字滚动</option>
</select>
</label>
<label>滚动速度:
<input type="range" id="scrollSpeed" min="1" max="100" value="30">
<span id="scrollSpeedValue">30</span>
</label>
<label>
<input type="checkbox" id="autoDuration"> 根据全文时长自动计算
</label>
<div id="durationSettings" style="display: none;">
<label>全文时长(分钟):
<input type="number" id="totalDuration" min="1" max="120" value="10">
</label>
</div>
</div>
</div>
<div class="settings-panel" id="fontSettings" style="display: none;">
<h3>字体设置</h3>
<div class="setting-group">
<label>字体:
<select id="fontFamily">
<option value="SimSun, serif">宋体</option>
<option value="Microsoft YaHei, sans-serif">微软雅黑</option>
<option value="SimHei, sans-serif">黑体</option>
<option value="KaiTi, serif">楷体</option>
</select>
</label>
<label>字号:
<input type="range" id="fontSize" min="12" max="72" value="16">
<span id="fontSizeValue">16px</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>
</label>
<label>右边距:
<input type="range" id="marginRight" min="0" max="200" value="50">
<span id="marginRightValue">50px</span>
</label>
<label>字间距:
<input type="range" id="letterSpacing" min="0" max="10" value="1">
<span id="letterSpacingValue">1px</span>
</label>
<label>行距:
<input type="range" id="lineHeight" min="1" max="3" step="0.1" value="1.6">
<span id="lineHeightValue">1.6</span>
</label>
</div>
</div>
<div class="settings-panel" id="flipSettings" style="display: none;">
<h3>翻转设置</h3>
<div class="setting-group">
<label>
<input type="checkbox" id="horizontalFlip"> 水平翻转
</label>
<label>
<input type="checkbox" id="verticalFlip"> 垂直翻转
</label>
</div>
</div>
<div class="settings-panel" id="watermarkSettings" style="display: none;">
<h3>水印设置</h3>
<div class="setting-group">
<label>
<input type="checkbox" id="watermarkEnabled"> 启用水印
</label>
<div id="watermarkOptions" style="display: none;">
<label>水印内容:
<input type="text" id="watermarkText" placeholder="输入水印文字">
</label>
<label>水印类型:
<select id="watermarkType">
<option value="center">大字居中背景</option>
<option value="diagonal">斜向暗纹</option>
</select>
</label>
</div>
</div>
</div>
<div class="settings-panel" id="recordSettings" style="display: none;">
<h3>录音设置</h3>
<div class="setting-group">
<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>
<!-- 隐藏的文件输入 -->
<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>
<script src="js/text.js"></script>
<script src="js/audio.js"></script>
<script src="js/settings.js"></script>
<script src="js/watermark.js"></script>
<script src="js/flip.js"></script>
</body>
</html>

@ -0,0 +1,205 @@
// 音频控制器
class AudioController {
constructor() {
this.mediaRecorder = null;
this.audioChunks = [];
this.isRecording = false;
this.audioBlob = null;
this.audioUrl = null;
this.recognition = null;
this.init();
}
init() {
this.setupRecordingControls();
this.initializeSpeechRecognition();
}
setupRecordingControls() {
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', () => {
this.startRecording();
});
stopBtn.addEventListener('click', () => {
this.stopRecording();
});
playBtn.addEventListener('click', () => {
this.playRecording();
});
saveBtn.addEventListener('click', () => {
this.saveRecording();
});
audioSyncCheckbox.addEventListener('change', () => {
this.toggleAudioSync(audioSyncCheckbox.checked);
});
}
async startRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.mediaRecorder = new MediaRecorder(stream);
this.audioChunks = [];
this.mediaRecorder.ondataavailable = (event) => {
this.audioChunks.push(event.data);
};
this.mediaRecorder.onstop = () => {
this.audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
this.audioUrl = URL.createObjectURL(this.audioBlob);
this.updateRecordingControls();
};
this.mediaRecorder.start();
this.isRecording = true;
this.updateRecordingControls();
// 开始语音识别(如果启用)
if (document.getElementById('audioSync').checked) {
this.startSpeechRecognition();
}
} catch (error) {
console.error('录音失败:', error);
alert('无法访问麦克风,请检查权限设置');
}
}
stopRecording() {
if (this.mediaRecorder && this.isRecording) {
this.mediaRecorder.stop();
this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
this.isRecording = false;
// 停止语音识别
if (this.recognition) {
this.recognition.stop();
}
}
}
playRecording() {
if (this.audioUrl) {
const audio = document.getElementById('audioPlayer');
audio.src = this.audioUrl;
audio.play();
}
}
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 {
startBtn.disabled = false;
stopBtn.disabled = true;
startBtn.textContent = '开始录音';
startBtn.style.background = '#007bff';
if (this.audioUrl) {
playBtn.disabled = false;
saveBtn.disabled = false;
}
}
}
initializeSpeechRecognition() {
// 检查浏览器支持
if ('webkitSpeechRecognition' in window) {
this.recognition = new webkitSpeechRecognition();
} else if ('SpeechRecognition' in window) {
this.recognition = new SpeechRecognition();
} else {
console.warn('当前浏览器不支持语音识别功能');
return;
}
if (this.recognition) {
this.recognition.lang = 'zh-CN';
this.recognition.continuous = true;
this.recognition.interimResults = true;
this.recognition.onresult = (event) => {
this.handleSpeechResult(event);
};
this.recognition.onerror = (event) => {
console.error('语音识别错误:', event.error);
};
}
}
startSpeechRecognition() {
if (this.recognition) {
this.recognition.start();
}
}
handleSpeechResult(event) {
const result = event.results[event.results.length - 1];
if (result.isFinal) {
const transcript = result[0].transcript;
this.syncScrollWithSpeech(transcript);
}
}
syncScrollWithSpeech(transcript) {
// 简化的语音同步逻辑
const editor = document.getElementById('textEditor');
const text = editor.textContent;
// 在文本中查找匹配的位置
const index = text.indexOf(transcript.trim());
if (index !== -1) {
// 计算滚动位置
const beforeText = text.substring(0, index);
const lines = beforeText.split('\n').length;
const totalLines = text.split('\n').length;
const scrollRatio = lines / totalLines;
// 滚动到对应位置
if (window.scrollController) {
window.scrollController.scrollToPosition(scrollRatio);
}
}
}
toggleAudioSync(enabled) {
if (enabled && !this.recognition) {
alert('当前浏览器不支持语音识别功能');
document.getElementById('audioSync').checked = false;
}
}
}

@ -0,0 +1,76 @@
// 翻转控制器
class FlipController {
constructor() {
this.init();
}
init() {
this.setupFlipControls();
}
setupFlipControls() {
const horizontalFlip = document.getElementById('horizontalFlip');
const verticalFlip = document.getElementById('verticalFlip');
horizontalFlip.addEventListener('change', () => {
this.updateFlip();
this.saveSettings();
});
verticalFlip.addEventListener('change', () => {
this.updateFlip();
this.saveSettings();
});
}
updateFlip() {
const editor = document.getElementById('textEditor');
const horizontalFlip = document.getElementById('horizontalFlip').checked;
const verticalFlip = document.getElementById('verticalFlip').checked;
// 清除所有翻转类
editor.classList.remove('flipped-horizontal', 'flipped-vertical', 'flipped-both');
// 应用新的翻转状态
if (horizontalFlip && verticalFlip) {
editor.classList.add('flipped-both');
} else if (horizontalFlip) {
editor.classList.add('flipped-horizontal');
} else if (verticalFlip) {
editor.classList.add('flipped-vertical');
}
// 更新翻转变换
this.applyFlipTransform(horizontalFlip, verticalFlip);
}
applyFlipTransform(horizontal, vertical) {
const editor = document.getElementById('textEditor');
let transform = '';
if (horizontal && vertical) {
transform = 'scale(-1, -1)';
} else if (horizontal) {
transform = 'scaleX(-1)';
} else if (vertical) {
transform = 'scaleY(-1)';
} else {
transform = 'none';
}
editor.style.transform = transform;
}
saveSettings() {
if (window.settingsController) {
window.settingsController.saveSettings();
}
}
resetFlip() {
document.getElementById('horizontalFlip').checked = false;
document.getElementById('verticalFlip').checked = false;
this.updateFlip();
this.saveSettings();
}
}

@ -0,0 +1,565 @@
// 主应用程序入口
class TeleprompterApp {
constructor() {
this.isPlaying = false;
this.isLocked = false;
this.currentSettings = this.getDefaultSettings();
this.scrollController = null;
this.activePanel = null;
this.currentLanguage = 'zh-CN';
this.sidebarHidden = false;
this.countdownDuration = 0;
this.init();
}
init() {
this.setupEventListeners();
this.loadDefaultText();
this.updateTimeDisplay();
this.initializeScrollbar();
this.setupSidebarBehavior();
// 每秒更新时间显示
setInterval(() => this.updateTimeDisplay(), 1000);
// 初始化各个模块
window.themeController = new ThemeController();
window.scrollController = new ScrollController();
window.textController = new TextController();
window.audioController = new AudioController();
window.settingsController = new SettingsController();
window.watermarkController = new WatermarkController();
window.flipController = new FlipController();
}
getDefaultSettings() {
return {
theme: 'dark',
fontSize: 16,
fontFamily: 'SimSun, serif',
fontColor: '#ffffff',
marginLeft: 50,
marginRight: 50,
letterSpacing: 1,
lineHeight: 1.6,
autoScroll: false,
scrollType: 'line',
scrollSpeed: 30,
horizontalFlip: false,
verticalFlip: false,
watermarkEnabled: false,
watermarkText: '',
watermarkType: 'center',
language: 'zh-CN',
countdownDuration: 0
};
}
setupEventListeners() {
// 全屏切换
document.getElementById('fullscreenBtn').addEventListener('click', () => {
this.toggleFullscreen();
});
// 主题切换
document.getElementById('themeBtn').addEventListener('click', () => {
window.themeController.toggleTheme();
});
// 播放/暂停 - 现在包含倒计时功能
document.getElementById('playBtn').addEventListener('click', () => {
this.showCountdownSettings();
});
// 语言切换
document.getElementById('languageBtn').addEventListener('click', () => {
this.toggleLanguage();
});
// 文本锁定
document.getElementById('lockBtn').addEventListener('click', () => {
this.toggleTextLock();
});
// 侧边栏隐藏/显示
document.getElementById('sidebarToggle').addEventListener('click', () => {
this.toggleSidebar();
});
// 设置面板切换
this.setupSettingsPanels();
// 文件导入
document.getElementById('importBtn').addEventListener('click', () => {
document.getElementById('fileInput').click();
});
document.getElementById('fileInput').addEventListener('change', (e) => {
window.textController.handleFileImport(e);
});
// 倒计时设置
document.getElementById('startCountdown').addEventListener('click', () => {
this.startCountdownAndPlay();
});
// 键盘快捷键
document.addEventListener('keydown', (e) => {
this.handleKeyboardShortcuts(e);
});
// 滚动检测
let scrollTimeout;
document.addEventListener('scroll', () => {
if (!this.sidebarHidden) {
document.body.classList.add('scrolling');
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
document.body.classList.remove('scrolling');
}, 1000);
}
});
// 鼠标滚轮事件
document.addEventListener('wheel', (e) => {
this.handleMouseWheel(e);
});
// 点击外部关闭设置面板
document.addEventListener('click', (e) => {
if (!e.target.closest('.settings-panel') && !e.target.closest('.sidebar-btn')) {
this.closeAllPanels();
}
});
}
setupSidebarBehavior() {
const sidebar = document.getElementById('sidebar');
const trigger = document.getElementById('sidebarTrigger');
// 鼠标移入按钮显示设置面板
const buttons = document.querySelectorAll('.sidebar-btn:not(.sidebar-toggle)');
buttons.forEach(btn => {
btn.addEventListener('mouseenter', () => {
if (!this.sidebarHidden) {
const panelId = this.getPanelIdForButton(btn.id);
if (panelId) {
this.showSettingsPanel(panelId, btn);
}
}
});
});
// 鼠标离开侧边栏区域隐藏面板
sidebar.addEventListener('mouseleave', () => {
setTimeout(() => {
if (!document.querySelector('.settings-panel:hover')) {
this.closeAllPanels();
}
}, 100);
});
// 左侧边缘触发显示
trigger.addEventListener('mouseenter', () => {
if (this.sidebarHidden) {
this.showSidebar();
}
});
}
getPanelIdForButton(buttonId) {
const mapping = {
'scrollBtn': 'scrollSettings',
'fontBtn': 'fontSettings',
'flipBtn': 'flipSettings',
'watermarkBtn': 'watermarkSettings',
'recordBtn': 'recordSettings'
};
return mapping[buttonId];
}
setupSettingsPanels() {
// 移除点击事件,改为鼠标移入事件
// 这些事件现在在 setupSidebarBehavior 中处理
}
showSettingsPanel(panelId, buttonElement) {
this.closeAllPanels();
const panel = document.getElementById(panelId);
if (!panel) return;
// 显示面板
panel.style.display = 'block';
this.activePanel = panelId;
// 定位面板
const rect = buttonElement.getBoundingClientRect();
panel.style.left = `${rect.right + 10}px`;
panel.style.top = `${rect.top}px`;
// 激活按钮样式
buttonElement.classList.add('active');
}
showCountdownSettings() {
this.closeAllPanels();
const panel = document.getElementById('countdownSettings');
const playBtn = document.getElementById('playBtn');
panel.style.display = 'block';
this.activePanel = 'countdownSettings';
// 定位面板
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');
const trigger = document.getElementById('sidebarTrigger');
const toggleBtn = document.getElementById('sidebarToggle');
const toggleIcon = toggleBtn.querySelector('img');
if (this.sidebarHidden) {
this.showSidebar();
} else {
this.hideSidebar();
}
}
showSidebar() {
const sidebar = document.getElementById('sidebar');
const trigger = document.getElementById('sidebarTrigger');
const toggleIcon = document.querySelector('#sidebarToggle img');
this.sidebarHidden = false;
sidebar.classList.remove('hidden');
trigger.classList.remove('active');
toggleIcon.src = 'images/chevron-left.svg';
document.getElementById('sidebarToggle').title = '隐藏功能区';
}
hideSidebar() {
const sidebar = document.getElementById('sidebar');
const trigger = document.getElementById('sidebarTrigger');
const toggleIcon = document.querySelector('#sidebarToggle img');
this.sidebarHidden = true;
sidebar.classList.add('hidden');
trigger.classList.add('active');
toggleIcon.src = 'images/chevron-right.svg';
document.getElementById('sidebarToggle').title = '显示功能区';
this.closeAllPanels();
}
closeAllPanels() {
const panels = document.querySelectorAll('.settings-panel');
const buttons = document.querySelectorAll('.sidebar-btn');
panels.forEach(panel => {
panel.style.display = 'none';
});
buttons.forEach(btn => {
btn.classList.remove('active');
});
this.activePanel = null;
}
toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().then(() => {
document.body.classList.add('fullscreen');
});
} else {
document.exitFullscreen().then(() => {
document.body.classList.remove('fullscreen');
});
}
}
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();
if (this.countdownDuration > 0) {
this.showCountdown().then(() => {
this.startReading();
});
} else {
this.startReading();
}
}
startReading() {
this.isPlaying = true;
this.updatePlayButton();
if (window.scrollController) {
window.scrollController.startAutoScroll();
}
if (window.audioController && window.audioController.isRecording) {
// 音频同步逻辑
}
}
pauseReading() {
this.isPlaying = false;
this.updatePlayButton();
if (window.scrollController) {
window.scrollController.stopAutoScroll();
}
}
updatePlayButton() {
const playBtn = document.getElementById('playBtn');
const icon = playBtn.querySelector('img');
if (this.isPlaying) {
icon.src = 'images/pause.svg';
playBtn.title = '暂停';
} else {
icon.src = 'images/play.svg';
playBtn.title = '播放/暂停';
}
}
showCountdown() {
return new Promise((resolve) => {
const countdown = document.getElementById('countdownDisplay');
const text = document.getElementById('countdownText');
let count = this.countdownDuration;
countdown.style.display = 'flex';
const timer = setInterval(() => {
text.textContent = count;
count--;
if (count < 0) {
clearInterval(timer);
countdown.style.display = 'none';
resolve();
}
}, 1000);
});
}
toggleTextLock() {
const editor = document.getElementById('textEditor');
const lockIcon = document.getElementById('lockIcon');
this.isLocked = !this.isLocked;
if (this.isLocked) {
editor.contentEditable = false;
editor.classList.add('locked');
lockIcon.src = 'images/lock.svg';
document.getElementById('lockBtn').title = '解锁文本';
} else {
editor.contentEditable = true;
editor.classList.remove('locked');
lockIcon.src = 'images/unlock.svg';
document.getElementById('lockBtn').title = '锁定文本';
}
}
handleKeyboardShortcuts(e) {
// Esc 键退出全屏
if (e.key === 'Escape' && document.fullscreenElement) {
document.exitFullscreen();
}
// 空格键播放/暂停
if (e.key === ' ' && !e.target.contentEditable) {
e.preventDefault();
if (this.isPlaying) {
this.pauseReading();
} else {
this.startCountdownAndPlay();
}
}
// F11 全屏
if (e.key === 'F11') {
e.preventDefault();
this.toggleFullscreen();
}
}
handleMouseWheel(e) {
const editor = document.getElementById('textEditor');
const delta = e.deltaY;
// 手动滚动不中断自动滚动,但会更新当前位置
if (window.scrollController && window.scrollController.isAutoScrolling) {
// 更新自动滚动的当前位置
window.scrollController.currentScrollPosition = editor.scrollTop + delta;
editor.scrollTop = window.scrollController.currentScrollPosition;
// 更新滚动条位置
this.updateScrollbar();
}
}
loadDefaultText() {
fetch('example.txt')
.then(response => response.text())
.then(text => {
window.textController.setText(text);
this.updateScrollbar();
})
.catch(error => {
console.error('加载默认文本失败:', error);
window.textController.setText('请导入文本文件或直接编辑此处内容。');
});
}
updateTimeDisplay() {
const now = new Date();
const timeString = now.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
document.getElementById('timeDisplay').textContent = timeString;
}
initializeScrollbar() {
const editor = document.getElementById('textEditor');
const scrollbar = document.getElementById('customScrollbar');
const thumb = document.getElementById('scrollbarThumb');
// 更新滚动条
const updateScrollbar = () => {
const scrollRatio = editor.scrollTop / (editor.scrollHeight - editor.clientHeight);
const thumbHeight = Math.max(20, (editor.clientHeight / editor.scrollHeight) * scrollbar.clientHeight);
const thumbTop = scrollRatio * (scrollbar.clientHeight - thumbHeight);
thumb.style.height = `${thumbHeight}px`;
thumb.style.transform = `translateY(${thumbTop}px)`;
};
// 监听文本编辑器滚动
editor.addEventListener('scroll', updateScrollbar);
// 拖拽滚动条
let isDragging = false;
thumb.addEventListener('mousedown', (e) => {
isDragging = true;
const startY = e.clientY;
const startScrollTop = editor.scrollTop;
const mouseMoveHandler = (e) => {
if (!isDragging) return;
const deltaY = e.clientY - startY;
const scrollRatio = deltaY / (scrollbar.clientHeight - thumb.clientHeight);
const newScrollTop = startScrollTop + scrollRatio * (editor.scrollHeight - editor.clientHeight);
editor.scrollTop = Math.max(0, Math.min(newScrollTop, editor.scrollHeight - editor.clientHeight));
};
const mouseUpHandler = () => {
isDragging = false;
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
});
// 点击滚动条跳转
scrollbar.addEventListener('click', (e) => {
if (e.target === thumb) return;
const clickRatio = e.offsetY / scrollbar.clientHeight;
const newScrollTop = clickRatio * (editor.scrollHeight - editor.clientHeight);
// 使用平滑滚动
if (window.scrollController) {
window.scrollController.smoothScrollTo(newScrollTop);
} else {
editor.scrollTop = newScrollTop;
}
});
// 初始更新
setTimeout(updateScrollbar, 100);
// 保存更新函数到实例
this.updateScrollbar = updateScrollbar;
}
}
// 添加CSS动画
const style = document.createElement('style');
style.textContent = `
@keyframes fadeInOut {
0% { opacity: 0; transform: translateY(-10px); }
20% { opacity: 1; transform: translateY(0); }
80% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-10px); }
}
`;
document.head.appendChild(style);
// 应用程序启动
document.addEventListener('DOMContentLoaded', () => {
window.app = new TeleprompterApp();
});

@ -0,0 +1,231 @@
// 滚动控制器
class ScrollController {
constructor() {
this.isAutoScrolling = false;
this.scrollSpeed = 30;
this.scrollType = 'line';
this.scrollInterval = null;
this.totalDuration = 10; // 分钟
this.autoCalculateDuration = false;
this.currentScrollPosition = 0;
this.targetScrollPosition = 0;
this.smoothScrollFrame = null;
this.init();
}
init() {
this.setupScrollSettings();
this.updateReadingLine();
}
setupScrollSettings() {
const autoScrollCheckbox = document.getElementById('autoScroll');
const autoScrollOptions = document.getElementById('autoScrollOptions');
const scrollTypeSelect = document.getElementById('scrollType');
const scrollSpeedRange = document.getElementById('scrollSpeed');
const scrollSpeedValue = document.getElementById('scrollSpeedValue');
const autoDurationCheckbox = document.getElementById('autoDuration');
const durationSettings = document.getElementById('durationSettings');
const totalDurationInput = document.getElementById('totalDuration');
// 自动滚动开关
autoScrollCheckbox.addEventListener('change', () => {
if (autoScrollCheckbox.checked) {
autoScrollOptions.style.display = 'block';
} else {
autoScrollOptions.style.display = 'none';
this.stopAutoScroll();
}
});
// 滚动类型选择
scrollTypeSelect.addEventListener('change', () => {
this.scrollType = scrollTypeSelect.value;
if (this.autoCalculateDuration) {
this.calculateOptimalSpeed();
}
});
// 滚动速度调整
scrollSpeedRange.addEventListener('input', () => {
this.scrollSpeed = parseInt(scrollSpeedRange.value);
scrollSpeedValue.textContent = this.scrollSpeed;
});
// 自动计算时长
autoDurationCheckbox.addEventListener('change', () => {
this.autoCalculateDuration = autoDurationCheckbox.checked;
if (autoDurationCheckbox.checked) {
durationSettings.style.display = 'block';
this.calculateOptimalSpeed();
} else {
durationSettings.style.display = 'none';
}
});
// 总时长设置
totalDurationInput.addEventListener('input', () => {
this.totalDuration = parseInt(totalDurationInput.value);
if (this.autoCalculateDuration) {
this.calculateOptimalSpeed();
}
});
}
startAutoScroll() {
if (this.isAutoScrolling) return;
const autoScrollEnabled = document.getElementById('autoScroll').checked;
if (!autoScrollEnabled) return;
this.isAutoScrolling = true;
const editor = document.getElementById('textEditor');
this.currentScrollPosition = editor.scrollTop;
this.smoothAutoScroll();
}
smoothAutoScroll() {
if (!this.isAutoScrolling) return;
const editor = document.getElementById('textEditor');
const maxScroll = editor.scrollHeight - editor.clientHeight;
if (this.currentScrollPosition >= maxScroll) {
this.stopAutoScroll();
return;
}
// 计算每帧的滚动距离,实现平滑滚动
const scrollStep = this.calculateScrollStep();
this.currentScrollPosition += scrollStep;
// 平滑更新滚动位置
editor.scrollTop = this.currentScrollPosition;
this.updateReadingLine();
if (window.app && window.app.updateScrollbar) {
window.app.updateScrollbar();
}
// 使用 requestAnimationFrame 实现平滑滚动
this.smoothScrollFrame = requestAnimationFrame(() => {
this.smoothAutoScroll();
});
}
calculateScrollStep() {
// 根据滚动速度和类型计算每帧的滚动距离
const baseSpeed = this.scrollSpeed / 100; // 将速度转换为0-1的比例
if (this.scrollType === 'line') {
// 按行滚动:基于行高计算
const editor = document.getElementById('textEditor');
const lineHeight = parseInt(window.getComputedStyle(editor).lineHeight) || 24;
return (lineHeight * baseSpeed) / 60; // 60fps
} else {
// 按字滚动:更细粒度的滚动
return baseSpeed * 2; // 每帧2像素的基础速度
}
}
stopAutoScroll() {
this.isAutoScrolling = false;
if (this.smoothScrollFrame) {
cancelAnimationFrame(this.smoothScrollFrame);
this.smoothScrollFrame = null;
}
}
calculateOptimalSpeed() {
const editor = document.getElementById('textEditor');
const text = editor.textContent;
let unitCount;
if (this.scrollType === 'line') {
unitCount = text.split('\n').length;
} else {
unitCount = text.length;
}
// 根据总时长计算最优速度
const totalSeconds = this.totalDuration * 60;
const optimalSpeed = Math.round((unitCount / totalSeconds) * 10);
this.scrollSpeed = Math.max(1, Math.min(100, optimalSpeed));
// 更新UI
const scrollSpeedRange = document.getElementById('scrollSpeed');
const scrollSpeedValue = document.getElementById('scrollSpeedValue');
scrollSpeedRange.value = this.scrollSpeed;
scrollSpeedValue.textContent = this.scrollSpeed;
}
updateReadingLine() {
const editor = document.getElementById('textEditor');
const readingLine = document.getElementById('readingLine');
// 计算当前阅读位置
const scrollRatio = editor.scrollTop / (editor.scrollHeight - editor.clientHeight);
const totalLines = editor.textContent.split('\n').length;
const currentLine = Math.floor(scrollRatio * totalLines);
// 高亮当前行
this.highlightCurrentLine(currentLine);
}
highlightCurrentLine(lineNumber) {
// 简化的高亮处理避免复杂的DOM操作
const editor = document.getElementById('textEditor');
const lines = editor.textContent.split('\n');
// 这里可以添加更复杂的高亮逻辑
// 由于contenteditable的复杂性暂时保持简单处理
}
scrollToPosition(ratio) {
const editor = document.getElementById('textEditor');
const targetScrollTop = ratio * (editor.scrollHeight - editor.clientHeight);
// 平滑滚动到目标位置
this.smoothScrollTo(targetScrollTop);
}
smoothScrollTo(targetPosition) {
const editor = document.getElementById('textEditor');
const startPosition = editor.scrollTop;
const distance = targetPosition - startPosition;
const duration = 500; // 500ms的滚动动画
let startTime = null;
const animateScroll = (currentTime) => {
if (startTime === null) startTime = currentTime;
const timeElapsed = currentTime - startTime;
const progress = Math.min(timeElapsed / duration, 1);
// 使用缓动函数实现平滑效果
const easeInOutQuad = progress < 0.5
? 2 * progress * progress
: 1 - Math.pow(-2 * progress + 2, 2) / 2;
editor.scrollTop = startPosition + distance * easeInOutQuad;
if (progress < 1) {
requestAnimationFrame(animateScroll);
} else {
this.updateReadingLine();
if (window.app && window.app.updateScrollbar) {
window.app.updateScrollbar();
}
}
};
requestAnimationFrame(animateScroll);
}
getScrollProgress() {
const editor = document.getElementById('textEditor');
return editor.scrollTop / (editor.scrollHeight - editor.clientHeight);
}
}

@ -0,0 +1,259 @@
// 设置控制器
class SettingsController {
constructor() {
this.init();
}
init() {
this.setupFontSettings();
this.loadSettings();
}
setupFontSettings() {
const fontFamily = document.getElementById('fontFamily');
const fontSize = document.getElementById('fontSize');
const fontSizeValue = document.getElementById('fontSizeValue');
const fontColor = document.getElementById('fontColor');
const marginLeft = document.getElementById('marginLeft');
const marginLeftValue = document.getElementById('marginLeftValue');
const marginRight = document.getElementById('marginRight');
const marginRightValue = document.getElementById('marginRightValue');
const letterSpacing = document.getElementById('letterSpacing');
const letterSpacingValue = document.getElementById('letterSpacingValue');
const lineHeight = document.getElementById('lineHeight');
const lineHeightValue = document.getElementById('lineHeightValue');
const editor = document.getElementById('textEditor');
// 字体系列
fontFamily.addEventListener('change', () => {
editor.style.fontFamily = fontFamily.value;
this.saveSettings();
});
// 字体大小
fontSize.addEventListener('input', () => {
const size = fontSize.value + 'px';
editor.style.fontSize = size;
fontSizeValue.textContent = size;
this.saveSettings();
});
// 字体颜色
fontColor.addEventListener('input', () => {
editor.style.color = fontColor.value;
this.saveSettings();
});
// 左边距
marginLeft.addEventListener('input', () => {
const margin = marginLeft.value + 'px';
editor.style.paddingLeft = margin;
marginLeftValue.textContent = margin;
this.saveSettings();
});
// 右边距
marginRight.addEventListener('input', () => {
const margin = marginRight.value + 'px';
editor.style.paddingRight = margin;
marginRightValue.textContent = margin;
this.saveSettings();
});
// 字间距
letterSpacing.addEventListener('input', () => {
const spacing = letterSpacing.value + 'px';
editor.style.letterSpacing = spacing;
letterSpacingValue.textContent = spacing;
this.saveSettings();
});
// 行距
lineHeight.addEventListener('input', () => {
const height = lineHeight.value;
editor.style.lineHeight = height;
lineHeightValue.textContent = height;
this.saveSettings();
});
}
saveSettings() {
const settings = {
fontFamily: document.getElementById('fontFamily').value,
fontSize: document.getElementById('fontSize').value,
fontColor: document.getElementById('fontColor').value,
marginLeft: document.getElementById('marginLeft').value,
marginRight: document.getElementById('marginRight').value,
letterSpacing: document.getElementById('letterSpacing').value,
lineHeight: document.getElementById('lineHeight').value,
autoScroll: document.getElementById('autoScroll').checked,
scrollType: document.getElementById('scrollType').value,
scrollSpeed: document.getElementById('scrollSpeed').value,
horizontalFlip: document.getElementById('horizontalFlip')?.checked || false,
verticalFlip: document.getElementById('verticalFlip')?.checked || false,
watermarkEnabled: document.getElementById('watermarkEnabled')?.checked || false,
watermarkText: document.getElementById('watermarkText')?.value || '',
watermarkType: document.getElementById('watermarkType')?.value || 'center'
};
localStorage.setItem('teleprompter-settings', JSON.stringify(settings));
}
loadSettings() {
const savedSettings = localStorage.getItem('teleprompter-settings');
if (!savedSettings) {
this.applyDefaultSettings();
return;
}
try {
const settings = JSON.parse(savedSettings);
this.applySettings(settings);
} catch (error) {
console.error('加载设置失败:', error);
this.applyDefaultSettings();
}
}
applyDefaultSettings() {
const defaultSettings = {
fontFamily: 'SimSun, serif',
fontSize: 16,
fontColor: window.themeController?.isDarkTheme() ? '#ffffff' : '#333333',
marginLeft: 50,
marginRight: 50,
letterSpacing: 1,
lineHeight: 1.6,
autoScroll: false,
scrollType: 'line',
scrollSpeed: 30,
horizontalFlip: false,
verticalFlip: false,
watermarkEnabled: false,
watermarkText: '',
watermarkType: 'center'
};
this.applySettings(defaultSettings);
}
applySettings(settings) {
const editor = document.getElementById('textEditor');
// 应用字体设置
if (settings.fontFamily) {
document.getElementById('fontFamily').value = settings.fontFamily;
editor.style.fontFamily = settings.fontFamily;
}
if (settings.fontSize) {
document.getElementById('fontSize').value = settings.fontSize;
document.getElementById('fontSizeValue').textContent = settings.fontSize + 'px';
editor.style.fontSize = settings.fontSize + 'px';
}
if (settings.fontColor) {
document.getElementById('fontColor').value = settings.fontColor;
editor.style.color = settings.fontColor;
}
if (settings.marginLeft !== undefined) {
document.getElementById('marginLeft').value = settings.marginLeft;
document.getElementById('marginLeftValue').textContent = settings.marginLeft + 'px';
editor.style.paddingLeft = settings.marginLeft + 'px';
}
if (settings.marginRight !== undefined) {
document.getElementById('marginRight').value = settings.marginRight;
document.getElementById('marginRightValue').textContent = settings.marginRight + 'px';
editor.style.paddingRight = settings.marginRight + 'px';
}
if (settings.letterSpacing !== undefined) {
document.getElementById('letterSpacing').value = settings.letterSpacing;
document.getElementById('letterSpacingValue').textContent = settings.letterSpacing + 'px';
editor.style.letterSpacing = settings.letterSpacing + 'px';
}
if (settings.lineHeight !== undefined) {
document.getElementById('lineHeight').value = settings.lineHeight;
document.getElementById('lineHeightValue').textContent = settings.lineHeight;
editor.style.lineHeight = settings.lineHeight;
}
// 应用滚动设置
if (settings.autoScroll !== undefined) {
document.getElementById('autoScroll').checked = settings.autoScroll;
}
if (settings.scrollType) {
document.getElementById('scrollType').value = settings.scrollType;
}
if (settings.scrollSpeed !== undefined) {
document.getElementById('scrollSpeed').value = settings.scrollSpeed;
document.getElementById('scrollSpeedValue').textContent = settings.scrollSpeed;
}
// 应用翻转设置
if (settings.horizontalFlip !== undefined && document.getElementById('horizontalFlip')) {
document.getElementById('horizontalFlip').checked = settings.horizontalFlip;
}
if (settings.verticalFlip !== undefined && document.getElementById('verticalFlip')) {
document.getElementById('verticalFlip').checked = settings.verticalFlip;
}
// 应用水印设置
if (settings.watermarkEnabled !== undefined && document.getElementById('watermarkEnabled')) {
document.getElementById('watermarkEnabled').checked = settings.watermarkEnabled;
}
if (settings.watermarkText && document.getElementById('watermarkText')) {
document.getElementById('watermarkText').value = settings.watermarkText;
}
if (settings.watermarkType && document.getElementById('watermarkType')) {
document.getElementById('watermarkType').value = settings.watermarkType;
}
}
resetSettings() {
if (confirm('确定要重置所有设置吗?')) {
localStorage.removeItem('teleprompter-settings');
this.applyDefaultSettings();
}
}
exportSettings() {
const settings = localStorage.getItem('teleprompter-settings');
if (settings) {
const blob = new Blob([settings], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `teleprompter-settings-${new Date().toISOString().slice(0, 10)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}
importSettings(file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const settings = JSON.parse(e.target.result);
this.applySettings(settings);
this.saveSettings();
alert('设置导入成功!');
} catch (error) {
alert('设置文件格式错误!');
}
};
reader.readAsText(file);
}
}

@ -0,0 +1,222 @@
// 文本控制器
class TextController {
constructor() {
this.init();
}
init() {
this.setupTextEditor();
}
setupTextEditor() {
const editor = document.getElementById('textEditor');
// 监听文本变化
editor.addEventListener('input', () => {
this.onTextChange();
});
// 监听粘贴事件
editor.addEventListener('paste', (e) => {
this.handlePaste(e);
});
// 监听拖拽文件
editor.addEventListener('dragover', (e) => {
e.preventDefault();
editor.classList.add('drag-over');
});
editor.addEventListener('dragleave', () => {
editor.classList.remove('drag-over');
});
editor.addEventListener('drop', (e) => {
e.preventDefault();
editor.classList.remove('drag-over');
this.handleFileDrop(e);
});
// 监听窗口大小变化,保持文本格式
window.addEventListener('resize', () => {
this.preserveTextFormat();
});
}
handleFileImport(event) {
const file = event.target.files[0];
if (!file) return;
if (!file.name.toLowerCase().endsWith('.txt')) {
alert('请选择.txt格式的文本文件');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target.result;
this.setText(text);
};
reader.readAsText(file, 'utf-8');
}
handleFileDrop(event) {
const files = event.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
if (file.name.toLowerCase().endsWith('.txt')) {
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target.result;
this.setText(text);
};
reader.readAsText(file, 'utf-8');
} else {
alert('请拖拽.txt格式的文本文件');
}
}
}
handlePaste(event) {
// 处理粘贴的纯文本,保持换行格式
event.preventDefault();
const text = (event.clipboardData || window.clipboardData).getData('text');
// 保持原有的换行符
const formattedText = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
// 创建文本节点并插入,保持换行
const lines = formattedText.split('\n');
for (let i = 0; i < lines.length; i++) {
if (i > 0) {
range.insertNode(document.createElement('br'));
}
if (lines[i]) {
range.insertNode(document.createTextNode(lines[i]));
}
}
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
this.onTextChange();
}
setText(text) {
const editor = document.getElementById('textEditor');
// 保持换行格式
const formattedText = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const lines = formattedText.split('\n');
editor.innerHTML = '';
for (let i = 0; i < lines.length; i++) {
if (i > 0) {
editor.appendChild(document.createElement('br'));
}
if (lines[i]) {
editor.appendChild(document.createTextNode(lines[i]));
}
}
this.onTextChange();
}
getText() {
const editor = document.getElementById('textEditor');
return editor.innerText || editor.textContent;
}
preserveTextFormat() {
// 在窗口大小变化时保持文本格式
const editor = document.getElementById('textEditor');
const currentText = this.getText();
// 重新设置文本以保持格式
setTimeout(() => {
this.setText(currentText);
}, 100);
}
onTextChange() {
// 更新滚动条
if (window.app && window.app.updateScrollbar) {
setTimeout(() => window.app.updateScrollbar(), 100);
}
// 如果启用了自动计算时长,重新计算
if (window.scrollController && window.scrollController.autoCalculateDuration) {
window.scrollController.calculateOptimalSpeed();
}
// 保存到本地存储
this.saveToLocalStorage();
}
saveToLocalStorage() {
const text = this.getText();
localStorage.setItem('teleprompter-text', text);
}
loadFromLocalStorage() {
const savedText = localStorage.getItem('teleprompter-text');
if (savedText) {
this.setText(savedText);
return true;
}
return false;
}
exportText() {
const text = this.getText();
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `teleprompter-text-${new Date().toISOString().slice(0, 10)}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
clearText() {
if (confirm('确定要清空所有文本吗?')) {
this.setText('');
}
}
insertAtCursor(text) {
const editor = document.getElementById('textEditor');
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(text));
range.collapse(false);
} else {
editor.appendChild(document.createTextNode(text));
}
this.onTextChange();
}
getWordCount() {
const text = this.getText();
return {
characters: text.length,
charactersNoSpaces: text.replace(/\s/g, '').length,
words: text.trim().split(/\s+/).filter(word => word.length > 0).length,
lines: text.split('\n').length,
paragraphs: text.split(/\n\s*\n/).filter(para => para.trim().length > 0).length
};
}
}

@ -0,0 +1,91 @@
// 主题控制器
class ThemeController {
constructor() {
this.currentTheme = 'dark';
this.init();
}
init() {
// 从本地存储加载主题设置
const savedTheme = localStorage.getItem('teleprompter-theme');
if (savedTheme) {
this.setTheme(savedTheme);
}
}
toggleTheme() {
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
this.setTheme(newTheme);
}
setTheme(theme) {
this.currentTheme = theme;
// 移除所有主题类
document.body.classList.remove('dark-theme', 'light-theme');
// 添加新主题类
document.body.classList.add(`${theme}-theme`);
// 更新按钮图标
this.updateThemeButton();
// 更新文字颜色
this.updateTextColor();
// 保存到本地存储
localStorage.setItem('teleprompter-theme', theme);
// 触发主题变化事件
this.dispatchThemeChange(theme);
}
updateThemeButton() {
const themeBtn = document.getElementById('themeBtn');
const icon = themeBtn.querySelector('img');
if (this.currentTheme === 'dark') {
icon.src = 'images/sun.svg';
themeBtn.title = '切换到浅色模式';
} else {
icon.src = 'images/moon.svg';
themeBtn.title = '切换到深色模式';
}
}
updateTextColor() {
const editor = document.getElementById('textEditor');
const fontColorInput = document.getElementById('fontColor');
if (this.currentTheme === 'dark') {
if (!fontColorInput.value || fontColorInput.value === '#333333') {
fontColorInput.value = '#ffffff';
editor.style.color = '#ffffff';
}
} else {
if (!fontColorInput.value || fontColorInput.value === '#ffffff') {
fontColorInput.value = '#333333';
editor.style.color = '#333333';
}
}
}
dispatchThemeChange(theme) {
const event = new CustomEvent('themeChanged', {
detail: { theme }
});
document.dispatchEvent(event);
}
getCurrentTheme() {
return this.currentTheme;
}
isDarkTheme() {
return this.currentTheme === 'dark';
}
isLightTheme() {
return this.currentTheme === 'light';
}
}

@ -0,0 +1,128 @@
// 水印控制器
class WatermarkController {
constructor() {
this.init();
}
init() {
this.setupWatermarkControls();
}
setupWatermarkControls() {
const watermarkEnabled = document.getElementById('watermarkEnabled');
const watermarkOptions = document.getElementById('watermarkOptions');
const watermarkText = document.getElementById('watermarkText');
const watermarkType = document.getElementById('watermarkType');
watermarkEnabled.addEventListener('change', () => {
if (watermarkEnabled.checked) {
watermarkOptions.style.display = 'block';
this.showWatermark();
} else {
watermarkOptions.style.display = 'none';
this.hideWatermark();
}
this.saveSettings();
});
watermarkText.addEventListener('input', () => {
this.updateWatermark();
this.saveSettings();
});
watermarkType.addEventListener('change', () => {
this.updateWatermark();
this.saveSettings();
});
}
showWatermark() {
const watermarkLayer = document.getElementById('watermarkLayer');
watermarkLayer.style.display = 'block';
this.updateWatermark();
}
hideWatermark() {
const watermarkLayer = document.getElementById('watermarkLayer');
watermarkLayer.style.display = 'none';
watermarkLayer.innerHTML = '';
}
updateWatermark() {
const watermarkEnabled = document.getElementById('watermarkEnabled');
if (!watermarkEnabled.checked) return;
const watermarkText = document.getElementById('watermarkText').value;
const watermarkType = document.getElementById('watermarkType').value;
const watermarkLayer = document.getElementById('watermarkLayer');
if (!watermarkText.trim()) {
watermarkLayer.innerHTML = '';
return;
}
if (watermarkType === 'center') {
this.createCenterWatermark(watermarkText, watermarkLayer);
} else {
this.createDiagonalWatermark(watermarkText, watermarkLayer);
}
}
createCenterWatermark(text, container) {
container.innerHTML = '';
const watermark = document.createElement('div');
watermark.textContent = text;
watermark.style.cssText = `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 120px;
font-weight: bold;
color: rgba(128, 128, 128, 0.1);
white-space: nowrap;
pointer-events: none;
z-index: 50;
user-select: none;
`;
container.appendChild(watermark);
}
createDiagonalWatermark(text, container) {
container.innerHTML = '';
const containerRect = container.getBoundingClientRect();
const numRows = Math.ceil(containerRect.height / 150);
const numCols = Math.ceil(containerRect.width / 300);
for (let row = 0; row < numRows; row++) {
for (let col = 0; col < numCols; col++) {
const watermark = document.createElement('div');
watermark.textContent = text;
watermark.style.cssText = `
position: absolute;
top: ${row * 150 + 75}px;
left: ${col * 300 + 150}px;
transform: translate(-50%, -50%) rotate(-45deg);
font-size: 24px;
font-weight: normal;
color: rgba(128, 128, 128, 0.08);
white-space: nowrap;
pointer-events: none;
z-index: 50;
user-select: none;
`;
container.appendChild(watermark);
}
}
}
saveSettings() {
if (window.settingsController) {
window.settingsController.saveSettings();
}
}
}

4051
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,33 @@
{
"name": "vite-react-typescript-starter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.344.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.18",
"eslint": "^9.9.1",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.11",
"globals": "^15.9.0",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.5.3",
"typescript-eslint": "^8.3.0",
"vite": "^5.4.2"
}
}

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

@ -0,0 +1,11 @@
import React from 'react';
function App() {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<p>Start prompting (or editing) to see magic happen :)</p>
</div>
);
}
export default App;

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

1
src/vite-env.d.ts vendored

@ -0,0 +1 @@
/// <reference types="vite/client" />

@ -0,0 +1,85 @@
/* 自定义滚动条 */
.custom-scrollbar {
position: fixed;
right: 10px;
top: 60px;
bottom: 20px;
width: 12px;
border-radius: 6px;
z-index: 800;
transition: all 0.3s ease;
}
.custom-scrollbar:hover {
width: 16px;
}
.scrollbar-thumb {
width: 100%;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
min-height: 20px;
}
.scrollbar-thumb:hover {
transform: scaleX(1.2);
}
/* 文本编辑器特殊状态 */
.text-editor.locked {
cursor: default;
user-select: none;
}
.text-editor.flipped-horizontal {
transform: scaleX(-1);
}
.text-editor.flipped-vertical {
transform: scaleY(-1);
}
.text-editor.flipped-both {
transform: scale(-1, -1);
}
.text-editor.auto-scrolling {
scroll-behavior: smooth;
}
/* 文本动画效果 */
.text-editor p {
margin-bottom: 0.8em;
transition: all 0.3s ease;
}
.text-editor p.highlight {
background: rgba(255, 255, 0, 0.2);
border-left: 3px solid #ffeb3b;
padding-left: 10px;
margin-left: -13px;
}
/* 全屏模式下的滚动条 */
.fullscreen .custom-scrollbar {
top: 20px;
}
/* 移动端文本编辑器 */
@media (max-width: 768px) {
.text-editor {
padding: 15px 30px;
font-size: 14px;
line-height: 1.5;
}
.custom-scrollbar {
width: 8px;
right: 5px;
}
.custom-scrollbar:hover {
width: 12px;
}
}

@ -0,0 +1,260 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'SimSun', serif;
font-weight: bold;
overflow: hidden;
height: 100vh;
position: relative;
transition: all 0.3s ease;
}
/* 顶部信息栏 */
.top-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
z-index: 1000;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.logo-area {
display: flex;
align-items: center;
gap: 10px;
}
.logo {
width: 32px;
height: 32px;
}
.project-name {
font-size: 18px;
font-weight: bold;
}
.time-display {
font-size: 16px;
font-family: 'Courier New', monospace;
min-width: 80px;
text-align: right;
}
/* 红色定位线 */
.reading-line {
position: fixed;
top: 25%;
left: 0;
right: 0;
height: 3px;
background: #ff4444;
z-index: 500;
box-shadow: 0 0 10px rgba(255, 68, 68, 0.5);
transition: opacity 0.3s ease;
}
/* 主内容区域 */
.main-content {
height: 100vh;
padding-top: 60px;
position: relative;
overflow: hidden;
}
.text-editor-container {
height: 100%;
position: relative;
display: flex;
}
.text-editor {
flex: 1;
padding: 20px 50px;
font-size: 16px;
line-height: 1.6;
letter-spacing: 1px;
outline: none;
border: none;
resize: none;
overflow-y: auto;
height: 100%;
transition: all 0.3s ease;
transform-origin: center;
}
.text-editor::-webkit-scrollbar {
display: none;
}
.text-editor.locked {
cursor: default;
user-select: none;
}
/* 水印层 */
.watermark-layer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 100;
overflow: hidden;
}
/* 设置面板通用样式 */
.settings-panel {
position: fixed;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
z-index: 1500;
min-width: 280px;
border: 1px solid rgba(255, 255, 255, 0.3);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.settings-panel h3 {
margin-bottom: 15px;
font-size: 16px;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
.setting-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.setting-group label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #555;
}
.setting-group input[type="range"] {
flex: 1;
margin: 0 8px;
}
.setting-group input[type="text"],
.setting-group input[type="number"],
.setting-group select {
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.setting-group input[type="color"] {
width: 40px;
height: 30px;
border: none;
border-radius: 6px;
cursor: pointer;
}
/* 录音控制按钮 */
.record-control-btn {
padding: 8px 16px;
margin: 4px;
border: none;
border-radius: 6px;
background: #007bff;
color: white;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.record-control-btn:hover:not(:disabled) {
background: #0056b3;
transform: translateY(-1px);
}
.record-control-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
/* 倒计时显示 */
.countdown-display {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2000;
background: rgba(0, 0, 0, 0.8);
color: white;
width: 100px;
height: 100px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
font-weight: bold;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.1); }
}
/* 全屏样式 */
.fullscreen .top-header {
display: none;
}
.fullscreen .main-content {
padding-top: 0;
}
.fullscreen .reading-line {
top: 25vh;
}
/* 响应式设计 */
@media (max-width: 768px) {
.settings-panel {
width: 90vw;
left: 5vw !important;
right: 5vw !important;
}
.text-editor {
padding: 15px 20px;
font-size: 14px;
}
}

@ -0,0 +1,153 @@
/* 左侧悬浮工具栏 */
.sidebar {
position: fixed;
left: 20px;
top: 50%;
transform: translateY(-50%);
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
transition: all 0.3s ease;
}
.sidebar.hidden {
opacity: 0;
transform: translateY(-50%) translateX(-100px);
pointer-events: none;
}
.sidebar.auto-hidden {
opacity: 0;
transform: translateY(-50%) translateX(-80px);
pointer-events: none;
}
.sidebar-btn {
width: 50px;
height: 50px;
border: none;
border-radius: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
position: relative;
overflow: hidden;
}
.sidebar-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.5s;
}
.sidebar-btn:hover::before {
left: 100%;
}
.sidebar-btn:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
}
.sidebar-btn:active {
transform: scale(0.95);
}
.sidebar-btn img {
width: 24px;
height: 24px;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
}
.sidebar-btn.active {
box-shadow: 0 0 20px rgba(0, 123, 255, 0.5);
border: 2px solid #007bff;
}
/* 隐藏/显示按钮特殊样式 */
.sidebar-toggle {
margin-top: 10px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
padding-top: 10px;
}
/* 左侧边缘触发区域 */
.sidebar-trigger {
position: fixed;
left: 0;
top: 0;
width: 10px;
height: 100vh;
z-index: 999;
background: transparent;
display: none;
}
.sidebar-trigger.active {
display: block;
}
/* 提示工具栏 - 改为鼠标移入显示 */
.sidebar-btn[title] {
position: relative;
}
.sidebar-btn[title]::after {
content: attr(title);
position: absolute;
left: calc(100% + 10px);
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
z-index: 1001;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.sidebar-btn[title]:hover::after {
opacity: 1;
}
/* 移动端适配 */
@media (max-width: 768px) {
.sidebar {
left: 10px;
gap: 8px;
}
.sidebar-btn {
width: 45px;
height: 45px;
}
.sidebar-btn img {
width: 20px;
height: 20px;
}
.sidebar-trigger {
width: 15px;
}
}
/* 滚动时自动隐藏 */
.scrolling .sidebar:not(.manual-hidden) {
opacity: 0.3;
transform: translateY(-50%) translateX(-20px);
}

@ -0,0 +1,112 @@
/* 浅色主题 */
.light-theme {
background-color: #EEF7FC;
color: #333333;
}
.light-theme .top-header {
background: rgba(238, 247, 252, 0.9);
color: #333333;
}
.light-theme .lock-btn {
background: rgba(255, 255, 255, 0.9);
color: #333333;
}
.light-theme .text-editor {
background: transparent;
color: #333333;
}
.light-theme .settings-panel {
background: rgba(255, 255, 255, 0.95);
color: #333333;
}
.light-theme .settings-panel h3 {
color: #333333;
}
.light-theme .setting-group label {
color: #555555;
}
.light-theme .custom-scrollbar {
background: rgba(0, 0, 0, 0.1);
}
.light-theme .scrollbar-thumb {
background: rgba(0, 0, 0, 0.4);
}
.light-theme .sidebar-btn {
background: rgba(255, 255, 255, 0.9);
color: #333333;
}
/* 深色主题 */
.dark-theme {
background-color: #000000;
color: #ffffff;
}
.dark-theme .top-header {
background: rgba(20, 20, 20, 0.9);
color: #ffffff;
}
.dark-theme .lock-btn {
background: rgba(40, 40, 40, 0.9);
color: #ffffff;
}
.dark-theme .text-editor {
background: transparent;
color: #ffffff;
}
.dark-theme .settings-panel {
background: rgba(30, 30, 30, 0.95);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.dark-theme .settings-panel h3 {
color: #ffffff;
border-bottom-color: #444;
}
.dark-theme .setting-group label {
color: #cccccc;
}
.dark-theme .setting-group input[type="text"],
.dark-theme .setting-group input[type="number"],
.dark-theme .setting-group select {
background: #444;
color: #fff;
border-color: #666;
}
.dark-theme .custom-scrollbar {
background: rgba(255, 255, 255, 0.1);
}
.dark-theme .scrollbar-thumb {
background: rgba(255, 255, 255, 0.4);
}
.dark-theme .sidebar-btn {
background: rgba(40, 40, 40, 0.9);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.dark-theme .record-control-btn {
background: #007bff;
}
.dark-theme .record-control-btn:hover:not(:disabled) {
background: #0056b3;
}

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
};

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
optimizeDeps: {
exclude: ['lucide-react'],
},
});
Loading…
Cancel
Save