@ -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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 351 B |
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 213 B |
After Width: | Height: | Size: 172 B |
After Width: | Height: | Size: 944 B |
After Width: | Height: | Size: 360 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 182 B |
After Width: | Height: | Size: 166 B |
After Width: | Height: | Size: 148 B |
After Width: | Height: | Size: 154 B |
After Width: | Height: | Size: 190 B |
After Width: | Height: | Size: 929 B |
After Width: | Height: | Size: 326 B |
After Width: | Height: | Size: 349 B |
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,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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
@ -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'],
|
||||
},
|
||||
});
|