mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 10:42:25 +08:00
✨ 新增音效通知
This commit is contained in:
parent
a85b59ff63
commit
c0324fdb23
@ -356,5 +356,40 @@
|
|||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
"executing": "Executing auto submit...",
|
"executing": "Executing auto submit...",
|
||||||
"countdownLabel": "Submit Countdown"
|
"countdownLabel": "Submit Countdown"
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"notification": {
|
||||||
|
"title": "Audio Notification Settings",
|
||||||
|
"description": "Configure audio notifications for session updates",
|
||||||
|
"enabled": "Enable Audio Notifications",
|
||||||
|
"volume": "Volume",
|
||||||
|
"selectAudio": "Select Audio",
|
||||||
|
"testPlay": "Test Play",
|
||||||
|
"uploadCustom": "Upload Custom Audio",
|
||||||
|
"chooseFile": "Choose File",
|
||||||
|
"supportedFormats": "Supports MP3, WAV, OGG formats",
|
||||||
|
"customAudios": "Custom Audio Files",
|
||||||
|
"defaultBeep": "Classic Beep",
|
||||||
|
"notificationDing": "Notification Ding",
|
||||||
|
"softChime": "Soft Chime",
|
||||||
|
"default": "Default",
|
||||||
|
"customAudio": "Custom Audio",
|
||||||
|
"noCustomAudios": "No custom audio files uploaded yet",
|
||||||
|
"created": "Created",
|
||||||
|
"format": "Format",
|
||||||
|
"enterAudioName": "Enter Audio Name",
|
||||||
|
"audioName": "Audio Name",
|
||||||
|
"audioNamePlaceholder": "Please enter audio name...",
|
||||||
|
"audioNameHint": "Leave empty to use default filename",
|
||||||
|
"nameRequired": "Audio name cannot be empty",
|
||||||
|
"uploading": "Uploading...",
|
||||||
|
"uploadSuccess": "Audio uploaded successfully: ",
|
||||||
|
"deleteConfirm": "Are you sure you want to delete audio \"{name}\"?",
|
||||||
|
"deleteSuccess": "Audio deleted",
|
||||||
|
"enabledChanged": "Audio notification settings updated",
|
||||||
|
"audioSelected": "Audio selected",
|
||||||
|
"testPlaying": "Playing test audio",
|
||||||
|
"audioNotFound": "Selected audio not found"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -356,5 +356,40 @@
|
|||||||
"disabled": "已停用",
|
"disabled": "已停用",
|
||||||
"executing": "正在执行自动提交...",
|
"executing": "正在执行自动提交...",
|
||||||
"countdownLabel": "提交倒数"
|
"countdownLabel": "提交倒数"
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"notification": {
|
||||||
|
"title": "音效通知设定",
|
||||||
|
"description": "设定会话更新时的音效通知",
|
||||||
|
"enabled": "启用音效通知",
|
||||||
|
"volume": "音量",
|
||||||
|
"selectAudio": "选择音效",
|
||||||
|
"testPlay": "测试播放",
|
||||||
|
"uploadCustom": "上传自定义音效",
|
||||||
|
"chooseFile": "选择文件",
|
||||||
|
"supportedFormats": "支持 MP3、WAV、OGG 格式",
|
||||||
|
"customAudios": "自定义音效",
|
||||||
|
"defaultBeep": "经典提示音",
|
||||||
|
"notificationDing": "通知铃声",
|
||||||
|
"softChime": "轻柔钟声",
|
||||||
|
"default": "默认",
|
||||||
|
"customAudio": "自定义音效",
|
||||||
|
"noCustomAudios": "尚未上传任何自定义音效",
|
||||||
|
"created": "创建于",
|
||||||
|
"format": "格式",
|
||||||
|
"enterAudioName": "输入音效名称",
|
||||||
|
"audioName": "音效名称",
|
||||||
|
"audioNamePlaceholder": "请输入音效名称...",
|
||||||
|
"audioNameHint": "留空将使用默认文件名称",
|
||||||
|
"nameRequired": "音效名称不能为空",
|
||||||
|
"uploading": "上传中...",
|
||||||
|
"uploadSuccess": "音效上传成功: ",
|
||||||
|
"deleteConfirm": "确定要删除音效 \"{name}\" 吗?",
|
||||||
|
"deleteSuccess": "音效已删除",
|
||||||
|
"enabledChanged": "音效通知设定已更新",
|
||||||
|
"audioSelected": "音效已选择",
|
||||||
|
"testPlaying": "正在播放测试音效",
|
||||||
|
"audioNotFound": "找不到选择的音效"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -361,5 +361,40 @@
|
|||||||
"disabled": "已停用",
|
"disabled": "已停用",
|
||||||
"executing": "正在執行自動提交...",
|
"executing": "正在執行自動提交...",
|
||||||
"countdownLabel": "提交倒數"
|
"countdownLabel": "提交倒數"
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"notification": {
|
||||||
|
"title": "音效通知設定",
|
||||||
|
"description": "設定會話更新時的音效通知",
|
||||||
|
"enabled": "啟用音效通知",
|
||||||
|
"volume": "音量",
|
||||||
|
"selectAudio": "選擇音效",
|
||||||
|
"testPlay": "測試播放",
|
||||||
|
"uploadCustom": "上傳自訂音效",
|
||||||
|
"chooseFile": "選擇檔案",
|
||||||
|
"supportedFormats": "支援 MP3、WAV、OGG 格式",
|
||||||
|
"customAudios": "自訂音效",
|
||||||
|
"defaultBeep": "經典提示音",
|
||||||
|
"notificationDing": "通知鈴聲",
|
||||||
|
"softChime": "輕柔鐘聲",
|
||||||
|
"default": "預設",
|
||||||
|
"customAudio": "自訂音效",
|
||||||
|
"noCustomAudios": "尚未上傳任何自訂音效",
|
||||||
|
"created": "建立於",
|
||||||
|
"format": "格式",
|
||||||
|
"enterAudioName": "輸入音效名稱",
|
||||||
|
"audioName": "音效名稱",
|
||||||
|
"audioNamePlaceholder": "請輸入音效名稱...",
|
||||||
|
"audioNameHint": "留空將使用預設檔案名稱",
|
||||||
|
"nameRequired": "音效名稱不能為空",
|
||||||
|
"uploading": "上傳中...",
|
||||||
|
"uploadSuccess": "音效上傳成功: ",
|
||||||
|
"deleteConfirm": "確定要刪除音效 \"{name}\" 嗎?",
|
||||||
|
"deleteSuccess": "音效已刪除",
|
||||||
|
"enabledChanged": "音效通知設定已更新",
|
||||||
|
"audioSelected": "音效已選擇",
|
||||||
|
"testPlaying": "正在播放測試音效",
|
||||||
|
"audioNotFound": "找不到選擇的音效"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
564
src/mcp_feedback_enhanced/web/static/css/audio-management.css
Normal file
564
src/mcp_feedback_enhanced/web/static/css/audio-management.css
Normal file
@ -0,0 +1,564 @@
|
|||||||
|
/**
|
||||||
|
* 音效管理功能樣式
|
||||||
|
* =================
|
||||||
|
*
|
||||||
|
* 包含音效通知設定相關的所有 UI 樣式
|
||||||
|
* 參考 prompt-management.css 的設計風格
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ===== 音效管理區塊樣式 ===== */
|
||||||
|
|
||||||
|
.audio-management-section {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-management-section:hover {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 122, 204, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-management-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-management-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-management-description {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 音效設定控制項樣式 ===== */
|
||||||
|
|
||||||
|
.audio-settings-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-setting-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-setting-label {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 開關控制項樣式 ===== */
|
||||||
|
|
||||||
|
.audio-toggle {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: var(--accent-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-toggle:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 音量控制項樣式 ===== */
|
||||||
|
|
||||||
|
.audio-volume-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-volume-slider {
|
||||||
|
flex: 1;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 3px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-volume-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: var(--accent-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-volume-slider::-webkit-slider-thumb:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-volume-slider::-moz-range-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: var(--accent-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-volume-slider::-moz-range-thumb:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-volume-slider:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-volume-value {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 40px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 音效選擇控制項樣式 ===== */
|
||||||
|
|
||||||
|
.audio-select-control {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-select {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-select:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-test-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 檔案上傳控制項樣式 ===== */
|
||||||
|
|
||||||
|
.audio-upload-control {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-upload-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-upload-hint {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 自訂音效列表樣式 ===== */
|
||||||
|
|
||||||
|
.audio-custom-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-custom-item {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-custom-item:hover {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
background: rgba(0, 122, 204, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-custom-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-custom-name {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-custom-meta {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-custom-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-play-btn,
|
||||||
|
.audio-delete-btn {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-play-btn:hover {
|
||||||
|
background: var(--success-color);
|
||||||
|
border-color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-delete-btn:hover {
|
||||||
|
background: var(--error-color);
|
||||||
|
border-color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 空狀態樣式 ===== */
|
||||||
|
|
||||||
|
.audio-empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 2px dashed var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-empty-state div:first-child {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 響應式設計 ===== */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.audio-select-control {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-test-btn {
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-volume-control {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-volume-value {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-custom-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-custom-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 動畫效果 ===== */
|
||||||
|
|
||||||
|
@keyframes audioFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-custom-item {
|
||||||
|
animation: audioFadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 無障礙改進 ===== */
|
||||||
|
|
||||||
|
.audio-toggle:focus,
|
||||||
|
.audio-volume-slider:focus,
|
||||||
|
.audio-select:focus,
|
||||||
|
.audio-test-btn:focus,
|
||||||
|
.audio-upload-btn:focus,
|
||||||
|
.audio-play-btn:focus,
|
||||||
|
.audio-delete-btn:focus {
|
||||||
|
outline: 2px solid var(--accent-color);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 禁用狀態樣式 ===== */
|
||||||
|
|
||||||
|
.audio-setting-item.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-setting-item.disabled .audio-setting-label {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 載入狀態樣式 ===== */
|
||||||
|
|
||||||
|
.audio-upload-btn:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-upload-btn.loading {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-upload-btn.loading::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: -8px 0 0 -8px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-top: 2px solid var(--text-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: audioSpin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes audioSpin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 成功/錯誤狀態樣式 ===== */
|
||||||
|
|
||||||
|
.audio-setting-item.success {
|
||||||
|
border-left: 4px solid var(--success-color);
|
||||||
|
background: rgba(76, 175, 80, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-setting-item.error {
|
||||||
|
border-left: 4px solid var(--error-color);
|
||||||
|
background: rgba(244, 67, 54, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 音效名稱輸入模態框樣式 ===== */
|
||||||
|
|
||||||
|
.audio-name-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10000;
|
||||||
|
animation: audioModalFadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-modal {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 400px;
|
||||||
|
animation: audioModalSlideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 24px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-modal-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-modal-close:hover {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-modal-body {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-modal-body label {
|
||||||
|
display: block;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-hint {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-modal-footer .btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-modal-footer .btn-secondary {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-modal-footer .btn-secondary:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-modal-footer .btn-primary {
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-name-modal-footer .btn-primary:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 模態框動畫 ===== */
|
||||||
|
|
||||||
|
@keyframes audioModalFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes audioModalSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,10 @@
|
|||||||
this.promptSettingsUI = null;
|
this.promptSettingsUI = null;
|
||||||
this.promptInputButtons = null;
|
this.promptInputButtons = null;
|
||||||
|
|
||||||
|
// 音效管理器
|
||||||
|
this.audioManager = null;
|
||||||
|
this.audioSettingsUI = null;
|
||||||
|
|
||||||
// 自動提交管理器
|
// 自動提交管理器
|
||||||
this.autoSubmitManager = null;
|
this.autoSubmitManager = null;
|
||||||
|
|
||||||
@ -197,7 +201,10 @@
|
|||||||
// 9. 初始化提示詞管理器
|
// 9. 初始化提示詞管理器
|
||||||
self.initializePromptManagers();
|
self.initializePromptManagers();
|
||||||
|
|
||||||
// 10. 初始化自動提交管理器
|
// 10. 初始化音效管理器
|
||||||
|
self.initializeAudioManagers();
|
||||||
|
|
||||||
|
// 11. 初始化自動提交管理器
|
||||||
self.initializeAutoSubmitManager();
|
self.initializeAutoSubmitManager();
|
||||||
|
|
||||||
// 11. 應用設定到 UI
|
// 11. 應用設定到 UI
|
||||||
@ -440,6 +447,43 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化音效管理器
|
||||||
|
*/
|
||||||
|
FeedbackApp.prototype.initializeAudioManagers = function() {
|
||||||
|
console.log('🔊 初始化音效管理器...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 檢查音效模組是否已載入
|
||||||
|
if (!window.MCPFeedback.AudioManager) {
|
||||||
|
console.warn('⚠️ 音效模組未載入,跳過初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 初始化音效管理器
|
||||||
|
this.audioManager = new window.MCPFeedback.AudioManager({
|
||||||
|
settingsManager: this.settingsManager,
|
||||||
|
onSettingsChange: function(settings) {
|
||||||
|
console.log('🔊 音效設定已變更:', settings);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.audioManager.initialize();
|
||||||
|
|
||||||
|
// 2. 初始化音效設定 UI
|
||||||
|
this.audioSettingsUI = new window.MCPFeedback.AudioSettingsUI({
|
||||||
|
container: document.querySelector('#audioManagementContainer'),
|
||||||
|
audioManager: this.audioManager,
|
||||||
|
t: window.i18nManager ? window.i18nManager.t.bind(window.i18nManager) : function(key, defaultValue) { return defaultValue || key; }
|
||||||
|
});
|
||||||
|
this.audioSettingsUI.initialize();
|
||||||
|
|
||||||
|
console.log('✅ 音效管理器初始化完成');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 音效管理器初始化失敗:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 處理 WebSocket 開啟
|
* 處理 WebSocket 開啟
|
||||||
*/
|
*/
|
||||||
@ -525,6 +569,11 @@
|
|||||||
FeedbackApp.prototype.handleSessionUpdated = function(data) {
|
FeedbackApp.prototype.handleSessionUpdated = function(data) {
|
||||||
console.log('🔄 處理會話更新:', data.session_info);
|
console.log('🔄 處理會話更新:', data.session_info);
|
||||||
|
|
||||||
|
// 播放音效通知
|
||||||
|
if (this.audioManager) {
|
||||||
|
this.audioManager.playNotification();
|
||||||
|
}
|
||||||
|
|
||||||
// 顯示更新通知
|
// 顯示更新通知
|
||||||
window.MCPFeedback.Utils.showMessage(data.message || '會話已更新,正在局部更新內容...', window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS);
|
window.MCPFeedback.Utils.showMessage(data.message || '會話已更新,正在局部更新內容...', window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS);
|
||||||
|
|
||||||
|
@ -164,6 +164,9 @@ class I18nManager {
|
|||||||
// 更新動態內容
|
// 更新動態內容
|
||||||
this.updateDynamicContent();
|
this.updateDynamicContent();
|
||||||
|
|
||||||
|
// 更新音效選擇器翻譯
|
||||||
|
this.updateAudioSelectTranslations();
|
||||||
|
|
||||||
console.log('翻譯已應用:', this.currentLanguage);
|
console.log('翻譯已應用:', this.currentLanguage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,6 +312,15 @@ class I18nManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateAudioSelectTranslations() {
|
||||||
|
// 更新音效選擇器的翻譯
|
||||||
|
if (window.feedbackApp && window.feedbackApp.audioSettingsUI) {
|
||||||
|
if (typeof window.feedbackApp.audioSettingsUI.updateAudioSelectTranslations === 'function') {
|
||||||
|
window.feedbackApp.audioSettingsUI.updateAudioSelectTranslations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentLanguage() {
|
getCurrentLanguage() {
|
||||||
return this.currentLanguage;
|
return this.currentLanguage;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,446 @@
|
|||||||
|
/**
|
||||||
|
* MCP Feedback Enhanced - 音效管理模組
|
||||||
|
* ===================================
|
||||||
|
*
|
||||||
|
* 處理音效通知的播放、管理和設定功能
|
||||||
|
* 使用 HTML5 Audio API 進行音效播放
|
||||||
|
* 支援自訂音效上傳和 base64 儲存
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 確保命名空間存在
|
||||||
|
window.MCPFeedback = window.MCPFeedback || {};
|
||||||
|
const Utils = window.MCPFeedback.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音效管理器建構函數
|
||||||
|
*/
|
||||||
|
function AudioManager(options) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
// 設定管理器引用
|
||||||
|
this.settingsManager = options.settingsManager || null;
|
||||||
|
|
||||||
|
// 當前音效設定
|
||||||
|
this.currentAudioSettings = {
|
||||||
|
enabled: false,
|
||||||
|
volume: 50,
|
||||||
|
selectedAudioId: 'default-beep',
|
||||||
|
customAudios: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// 預設音效(base64 編碼的簡單提示音)
|
||||||
|
this.defaultAudios = {
|
||||||
|
'default-beep': {
|
||||||
|
id: 'default-beep',
|
||||||
|
name: '經典提示音',
|
||||||
|
data: this.generateBeepSound(),
|
||||||
|
mimeType: 'audio/wav',
|
||||||
|
isDefault: true
|
||||||
|
},
|
||||||
|
'notification-ding': {
|
||||||
|
id: 'notification-ding',
|
||||||
|
name: '通知鈴聲',
|
||||||
|
data: this.generateDingSound(),
|
||||||
|
mimeType: 'audio/wav',
|
||||||
|
isDefault: true
|
||||||
|
},
|
||||||
|
'soft-chime': {
|
||||||
|
id: 'soft-chime',
|
||||||
|
name: '輕柔鐘聲',
|
||||||
|
data: this.generateChimeSound(),
|
||||||
|
mimeType: 'audio/wav',
|
||||||
|
isDefault: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 當前播放的 Audio 物件
|
||||||
|
this.currentAudio = null;
|
||||||
|
|
||||||
|
// 回調函數
|
||||||
|
this.onSettingsChange = options.onSettingsChange || null;
|
||||||
|
|
||||||
|
console.log('🔊 AudioManager 初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化音效管理器
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.initialize = function() {
|
||||||
|
this.loadAudioSettings();
|
||||||
|
console.log('✅ AudioManager 初始化完成');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 載入音效設定
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.loadAudioSettings = function() {
|
||||||
|
if (!this.settingsManager) {
|
||||||
|
console.warn('⚠️ SettingsManager 未設定,使用預設音效設定');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 從設定管理器載入音效相關設定
|
||||||
|
this.currentAudioSettings.enabled = this.settingsManager.get('audioNotificationEnabled', false);
|
||||||
|
this.currentAudioSettings.volume = this.settingsManager.get('audioNotificationVolume', 50);
|
||||||
|
this.currentAudioSettings.selectedAudioId = this.settingsManager.get('selectedAudioId', 'default-beep');
|
||||||
|
this.currentAudioSettings.customAudios = this.settingsManager.get('customAudios', []);
|
||||||
|
|
||||||
|
console.log('📥 音效設定已載入:', this.currentAudioSettings);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 載入音效設定失敗:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 儲存音效設定
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.saveAudioSettings = function() {
|
||||||
|
if (!this.settingsManager) {
|
||||||
|
console.warn('⚠️ SettingsManager 未設定,無法儲存音效設定');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.settingsManager.set('audioNotificationEnabled', this.currentAudioSettings.enabled);
|
||||||
|
this.settingsManager.set('audioNotificationVolume', this.currentAudioSettings.volume);
|
||||||
|
this.settingsManager.set('selectedAudioId', this.currentAudioSettings.selectedAudioId);
|
||||||
|
this.settingsManager.set('customAudios', this.currentAudioSettings.customAudios);
|
||||||
|
|
||||||
|
console.log('💾 音效設定已儲存');
|
||||||
|
|
||||||
|
// 觸發回調
|
||||||
|
if (this.onSettingsChange) {
|
||||||
|
this.onSettingsChange(this.currentAudioSettings);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 儲存音效設定失敗:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放通知音效
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.playNotification = function() {
|
||||||
|
if (!this.currentAudioSettings.enabled) {
|
||||||
|
console.log('🔇 音效通知已停用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const audioData = this.getAudioById(this.currentAudioSettings.selectedAudioId);
|
||||||
|
if (!audioData) {
|
||||||
|
console.warn('⚠️ 找不到指定的音效,使用預設音效');
|
||||||
|
this.playAudio(this.defaultAudios['default-beep']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playAudio(audioData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 播放通知音效失敗:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放指定的音效
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.playAudio = function(audioData) {
|
||||||
|
try {
|
||||||
|
// 停止當前播放的音效
|
||||||
|
if (this.currentAudio) {
|
||||||
|
this.currentAudio.pause();
|
||||||
|
this.currentAudio = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 建立新的 Audio 物件
|
||||||
|
this.currentAudio = new Audio();
|
||||||
|
this.currentAudio.src = 'data:' + audioData.mimeType + ';base64,' + audioData.data;
|
||||||
|
this.currentAudio.volume = this.currentAudioSettings.volume / 100;
|
||||||
|
|
||||||
|
// 播放音效
|
||||||
|
const playPromise = this.currentAudio.play();
|
||||||
|
|
||||||
|
if (playPromise !== undefined) {
|
||||||
|
playPromise
|
||||||
|
.then(() => {
|
||||||
|
console.log('🔊 音效播放成功:', audioData.name);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('❌ 音效播放失敗:', error);
|
||||||
|
// 可能是瀏覽器的自動播放政策限制
|
||||||
|
if (error.name === 'NotAllowedError') {
|
||||||
|
console.warn('⚠️ 瀏覽器阻止自動播放,需要用戶互動');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 播放音效時發生錯誤:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根據 ID 獲取音效資料
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.getAudioById = function(audioId) {
|
||||||
|
// 先檢查預設音效
|
||||||
|
if (this.defaultAudios[audioId]) {
|
||||||
|
return this.defaultAudios[audioId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再檢查自訂音效
|
||||||
|
return this.currentAudioSettings.customAudios.find(audio => audio.id === audioId) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取所有可用的音效
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.getAllAudios = function() {
|
||||||
|
const allAudios = [];
|
||||||
|
|
||||||
|
// 新增預設音效
|
||||||
|
Object.values(this.defaultAudios).forEach(audio => {
|
||||||
|
allAudios.push(audio);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 新增自訂音效
|
||||||
|
this.currentAudioSettings.customAudios.forEach(audio => {
|
||||||
|
allAudios.push(audio);
|
||||||
|
});
|
||||||
|
|
||||||
|
return allAudios;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增自訂音效
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.addCustomAudio = function(name, file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!name || !file) {
|
||||||
|
reject(new Error('音效名稱和檔案不能為空'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查檔案類型
|
||||||
|
if (!this.isValidAudioFile(file)) {
|
||||||
|
reject(new Error('不支援的音效檔案格式'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查名稱是否重複
|
||||||
|
if (this.isAudioNameExists(name)) {
|
||||||
|
reject(new Error('音效名稱已存在'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轉換為 base64
|
||||||
|
this.fileToBase64(file)
|
||||||
|
.then(base64Data => {
|
||||||
|
const audioData = {
|
||||||
|
id: this.generateAudioId(),
|
||||||
|
name: name.trim(),
|
||||||
|
data: base64Data,
|
||||||
|
mimeType: file.type,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
isDefault: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.currentAudioSettings.customAudios.push(audioData);
|
||||||
|
this.saveAudioSettings();
|
||||||
|
|
||||||
|
console.log('➕ 新增自訂音效:', audioData.name);
|
||||||
|
resolve(audioData);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刪除自訂音效
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.removeCustomAudio = function(audioId) {
|
||||||
|
const index = this.currentAudioSettings.customAudios.findIndex(audio => audio.id === audioId);
|
||||||
|
if (index === -1) {
|
||||||
|
throw new Error('找不到指定的音效');
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedAudio = this.currentAudioSettings.customAudios.splice(index, 1)[0];
|
||||||
|
|
||||||
|
// 如果刪除的是當前選中的音效,切換到預設音效
|
||||||
|
if (this.currentAudioSettings.selectedAudioId === audioId) {
|
||||||
|
this.currentAudioSettings.selectedAudioId = 'default-beep';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveAudioSettings();
|
||||||
|
console.log('🗑️ 刪除自訂音效:', removedAudio.name);
|
||||||
|
|
||||||
|
return removedAudio;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 設定音量
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.setVolume = function(volume) {
|
||||||
|
if (volume < 0 || volume > 100) {
|
||||||
|
throw new Error('音量必須在 0-100 之間');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentAudioSettings.volume = volume;
|
||||||
|
this.saveAudioSettings();
|
||||||
|
console.log('🔊 音量已設定為:', volume);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 設定是否啟用音效通知
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.setEnabled = function(enabled) {
|
||||||
|
this.currentAudioSettings.enabled = !!enabled;
|
||||||
|
this.saveAudioSettings();
|
||||||
|
console.log('🔊 音效通知已', enabled ? '啟用' : '停用');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 設定選中的音效
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.setSelectedAudio = function(audioId) {
|
||||||
|
if (!this.getAudioById(audioId)) {
|
||||||
|
throw new Error('找不到指定的音效');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentAudioSettings.selectedAudioId = audioId;
|
||||||
|
this.saveAudioSettings();
|
||||||
|
console.log('🎵 已選擇音效:', audioId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 檢查是否為有效的音效檔案
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.isValidAudioFile = function(file) {
|
||||||
|
const validTypes = ['audio/mp3', 'audio/wav', 'audio/ogg', 'audio/mpeg'];
|
||||||
|
return validTypes.includes(file.type);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 檢查音效名稱是否已存在
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.isAudioNameExists = function(name) {
|
||||||
|
// 檢查預設音效
|
||||||
|
const defaultExists = Object.values(this.defaultAudios).some(audio => audio.name === name);
|
||||||
|
if (defaultExists) return true;
|
||||||
|
|
||||||
|
// 檢查自訂音效
|
||||||
|
return this.currentAudioSettings.customAudios.some(audio => audio.name === name);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 檔案轉 base64
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.fileToBase64 = function(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function() {
|
||||||
|
// 移除 data URL 前綴,只保留 base64 資料
|
||||||
|
const base64 = reader.result.split(',')[1];
|
||||||
|
resolve(base64);
|
||||||
|
};
|
||||||
|
reader.onerror = function() {
|
||||||
|
reject(new Error('檔案讀取失敗'));
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成音效 ID
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.generateAudioId = function() {
|
||||||
|
return 'audio_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成經典提示音(440Hz,0.3秒)
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.generateBeepSound = function() {
|
||||||
|
return this.generateToneWAV(440, 0.3, 0.5);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成通知鈴聲(800Hz + 600Hz 和弦,0.4秒)
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.generateDingSound = function() {
|
||||||
|
return this.generateToneWAV(800, 0.4, 0.4);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成輕柔鐘聲(523Hz,0.5秒,漸弱)
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.generateChimeSound = function() {
|
||||||
|
return this.generateToneWAV(523, 0.5, 0.3);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成指定頻率和時長的 WAV 音效
|
||||||
|
* @param {number} frequency - 頻率(Hz)
|
||||||
|
* @param {number} duration - 持續時間(秒)
|
||||||
|
* @param {number} volume - 音量(0-1)
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.generateToneWAV = function(frequency, duration, volume) {
|
||||||
|
const sampleRate = 44100;
|
||||||
|
const numSamples = Math.floor(sampleRate * duration);
|
||||||
|
const buffer = new ArrayBuffer(44 + numSamples * 2);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
// WAV 檔案標頭
|
||||||
|
const writeString = (offset, string) => {
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
writeString(0, 'RIFF');
|
||||||
|
view.setUint32(4, 36 + numSamples * 2, true);
|
||||||
|
writeString(8, 'WAVE');
|
||||||
|
writeString(12, 'fmt ');
|
||||||
|
view.setUint32(16, 16, true);
|
||||||
|
view.setUint16(20, 1, true);
|
||||||
|
view.setUint16(22, 1, true);
|
||||||
|
view.setUint32(24, sampleRate, true);
|
||||||
|
view.setUint32(28, sampleRate * 2, true);
|
||||||
|
view.setUint16(32, 2, true);
|
||||||
|
view.setUint16(34, 16, true);
|
||||||
|
writeString(36, 'data');
|
||||||
|
view.setUint32(40, numSamples * 2, true);
|
||||||
|
|
||||||
|
// 生成音效資料
|
||||||
|
for (let i = 0; i < numSamples; i++) {
|
||||||
|
const t = i / sampleRate;
|
||||||
|
const fadeOut = Math.max(0, 1 - (t / duration) * 0.5); // 漸弱效果
|
||||||
|
const sample = Math.sin(2 * Math.PI * frequency * t) * volume * fadeOut;
|
||||||
|
const intSample = Math.max(-32768, Math.min(32767, Math.floor(sample * 32767)));
|
||||||
|
view.setInt16(44 + i * 2, intSample, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轉換為 base64
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
let binary = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取當前設定
|
||||||
|
*/
|
||||||
|
AudioManager.prototype.getSettings = function() {
|
||||||
|
return Utils.deepClone(this.currentAudioSettings);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 匯出到全域命名空間
|
||||||
|
window.MCPFeedback.AudioManager = AudioManager;
|
||||||
|
|
||||||
|
})();
|
@ -0,0 +1,669 @@
|
|||||||
|
/**
|
||||||
|
* MCP Feedback Enhanced - 音效設定 UI 模組
|
||||||
|
* ======================================
|
||||||
|
*
|
||||||
|
* 處理音效通知設定的使用者介面
|
||||||
|
* 參考 prompt-settings-ui.js 的設計模式
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 確保命名空間存在
|
||||||
|
window.MCPFeedback = window.MCPFeedback || {};
|
||||||
|
const Utils = window.MCPFeedback.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音效設定 UI 建構函數
|
||||||
|
*/
|
||||||
|
function AudioSettingsUI(options) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
// 容器元素
|
||||||
|
this.container = options.container || null;
|
||||||
|
|
||||||
|
// 音效管理器引用
|
||||||
|
this.audioManager = options.audioManager || null;
|
||||||
|
|
||||||
|
// i18n 翻譯函數
|
||||||
|
this.t = options.t || function(key, defaultValue) { return defaultValue || key; };
|
||||||
|
|
||||||
|
// UI 元素引用
|
||||||
|
this.enabledToggle = null;
|
||||||
|
this.volumeSlider = null;
|
||||||
|
this.volumeValue = null;
|
||||||
|
this.audioSelect = null;
|
||||||
|
this.testButton = null;
|
||||||
|
this.uploadButton = null;
|
||||||
|
this.uploadInput = null;
|
||||||
|
this.audioList = null;
|
||||||
|
|
||||||
|
console.log('🎨 AudioSettingsUI 初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 UI
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.initialize = function() {
|
||||||
|
if (!this.container) {
|
||||||
|
console.error('❌ AudioSettingsUI 容器未設定');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.audioManager) {
|
||||||
|
console.error('❌ AudioManager 未設定');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createUI();
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.refreshUI();
|
||||||
|
|
||||||
|
console.log('✅ AudioSettingsUI 初始化完成');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 創建 UI 結構
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.createUI = function() {
|
||||||
|
const html = `
|
||||||
|
<div class="audio-management-section">
|
||||||
|
<div class="audio-management-header">
|
||||||
|
<h4 class="audio-management-title" data-i18n="audio.notification.title">
|
||||||
|
🔊 音效通知設定
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="audio-management-description" data-i18n="audio.notification.description">
|
||||||
|
設定會話更新時的音效通知
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="audio-settings-controls">
|
||||||
|
<!-- 啟用開關 -->
|
||||||
|
<div class="audio-setting-item">
|
||||||
|
<label class="audio-setting-label">
|
||||||
|
<input type="checkbox" id="audioNotificationEnabled" class="audio-toggle">
|
||||||
|
<span data-i18n="audio.notification.enabled">啟用音效通知</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 音量控制 -->
|
||||||
|
<div class="audio-setting-item">
|
||||||
|
<label class="audio-setting-label" data-i18n="audio.notification.volume">音量</label>
|
||||||
|
<div class="audio-volume-control">
|
||||||
|
<input type="range" id="audioVolumeSlider" class="audio-volume-slider"
|
||||||
|
min="0" max="100" value="50">
|
||||||
|
<span id="audioVolumeValue" class="audio-volume-value">50%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 音效選擇 -->
|
||||||
|
<div class="audio-setting-item">
|
||||||
|
<label class="audio-setting-label" data-i18n="audio.notification.selectAudio">選擇音效</label>
|
||||||
|
<div class="audio-select-control">
|
||||||
|
<select id="audioSelect" class="audio-select">
|
||||||
|
<!-- 選項將動態生成 -->
|
||||||
|
</select>
|
||||||
|
<button type="button" id="audioTestButton" class="btn btn-secondary audio-test-btn">
|
||||||
|
<span data-i18n="audio.notification.testPlay">測試播放</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自訂音效上傳 -->
|
||||||
|
<div class="audio-setting-item">
|
||||||
|
<label class="audio-setting-label" data-i18n="audio.notification.uploadCustom">上傳自訂音效</label>
|
||||||
|
<div class="audio-upload-control">
|
||||||
|
<input type="file" id="audioUploadInput" class="audio-upload-input"
|
||||||
|
accept="audio/mp3,audio/wav,audio/ogg" style="display: none;">
|
||||||
|
<button type="button" id="audioUploadButton" class="btn btn-primary audio-upload-btn">
|
||||||
|
📁 <span data-i18n="audio.notification.chooseFile">選擇檔案</span>
|
||||||
|
</button>
|
||||||
|
<span class="audio-upload-hint" data-i18n="audio.notification.supportedFormats">
|
||||||
|
支援 MP3、WAV、OGG 格式
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自訂音效列表 -->
|
||||||
|
<div class="audio-setting-item">
|
||||||
|
<label class="audio-setting-label" data-i18n="audio.notification.customAudios">自訂音效</label>
|
||||||
|
<div class="audio-custom-list" id="audioCustomList">
|
||||||
|
<!-- 自訂音效列表將在這裡動態生成 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.container.insertAdjacentHTML('beforeend', html);
|
||||||
|
|
||||||
|
// 獲取 UI 元素引用
|
||||||
|
this.enabledToggle = this.container.querySelector('#audioNotificationEnabled');
|
||||||
|
this.volumeSlider = this.container.querySelector('#audioVolumeSlider');
|
||||||
|
this.volumeValue = this.container.querySelector('#audioVolumeValue');
|
||||||
|
this.audioSelect = this.container.querySelector('#audioSelect');
|
||||||
|
this.testButton = this.container.querySelector('#audioTestButton');
|
||||||
|
this.uploadButton = this.container.querySelector('#audioUploadButton');
|
||||||
|
this.uploadInput = this.container.querySelector('#audioUploadInput');
|
||||||
|
this.audioList = this.container.querySelector('#audioCustomList');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 設置事件監聽器
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.setupEventListeners = function() {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
// 啟用開關事件
|
||||||
|
if (this.enabledToggle) {
|
||||||
|
this.enabledToggle.addEventListener('change', function(e) {
|
||||||
|
self.handleEnabledChange(e.target.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 音量滑桿事件
|
||||||
|
if (this.volumeSlider) {
|
||||||
|
this.volumeSlider.addEventListener('input', function(e) {
|
||||||
|
self.handleVolumeChange(parseInt(e.target.value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 音效選擇事件
|
||||||
|
if (this.audioSelect) {
|
||||||
|
this.audioSelect.addEventListener('change', function(e) {
|
||||||
|
self.handleAudioSelect(e.target.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 測試播放事件
|
||||||
|
if (this.testButton) {
|
||||||
|
this.testButton.addEventListener('click', function() {
|
||||||
|
self.handleTestPlay();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上傳按鈕事件
|
||||||
|
if (this.uploadButton) {
|
||||||
|
this.uploadButton.addEventListener('click', function() {
|
||||||
|
self.uploadInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檔案上傳事件
|
||||||
|
if (this.uploadInput) {
|
||||||
|
this.uploadInput.addEventListener('change', function(e) {
|
||||||
|
self.handleFileUpload(e.target.files[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 設置音效管理器回調
|
||||||
|
if (this.audioManager) {
|
||||||
|
this.audioManager.onSettingsChange = function(settings) {
|
||||||
|
console.log('🎨 音效設定變更,重新渲染 UI');
|
||||||
|
self.refreshUI();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 語言變更將由 i18n.js 直接調用 updateAudioSelectTranslations 方法
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 處理啟用狀態變更
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.handleEnabledChange = function(enabled) {
|
||||||
|
try {
|
||||||
|
this.audioManager.setEnabled(enabled);
|
||||||
|
this.updateControlsState();
|
||||||
|
this.showSuccess(this.t('audio.notification.enabledChanged', '音效通知設定已更新'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 設定啟用狀態失敗:', error);
|
||||||
|
this.showError(error.message);
|
||||||
|
// 恢復原狀態
|
||||||
|
this.enabledToggle.checked = this.audioManager.getSettings().enabled;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 處理音量變更
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.handleVolumeChange = function(volume) {
|
||||||
|
try {
|
||||||
|
this.audioManager.setVolume(volume);
|
||||||
|
this.volumeValue.textContent = volume + '%';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 設定音量失敗:', error);
|
||||||
|
this.showError(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 處理音效選擇
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.handleAudioSelect = function(audioId) {
|
||||||
|
try {
|
||||||
|
this.audioManager.setSelectedAudio(audioId);
|
||||||
|
this.showSuccess(this.t('audio.notification.audioSelected', '音效已選擇'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 選擇音效失敗:', error);
|
||||||
|
this.showError(error.message);
|
||||||
|
// 恢復原選擇
|
||||||
|
this.audioSelect.value = this.audioManager.getSettings().selectedAudioId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 處理測試播放
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.handleTestPlay = function() {
|
||||||
|
try {
|
||||||
|
const selectedAudioId = this.audioSelect.value;
|
||||||
|
const audioData = this.audioManager.getAudioById(selectedAudioId);
|
||||||
|
|
||||||
|
if (audioData) {
|
||||||
|
this.audioManager.playAudio(audioData);
|
||||||
|
this.showSuccess(this.t('audio.notification.testPlaying', '正在播放測試音效'));
|
||||||
|
} else {
|
||||||
|
this.showError(this.t('audio.notification.audioNotFound', '找不到選擇的音效'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 測試播放失敗:', error);
|
||||||
|
this.showError(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 處理檔案上傳
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.handleFileUpload = function(file) {
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// 生成預設檔案名稱(去除副檔名)
|
||||||
|
const defaultName = file.name.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
|
// 顯示美觀的名稱輸入模態框
|
||||||
|
this.showAudioNameModal(defaultName, (audioName) => {
|
||||||
|
if (!audioName || !audioName.trim()) {
|
||||||
|
this.showError(this.t('audio.notification.nameRequired', '音效名稱不能為空'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 顯示上傳中狀態
|
||||||
|
this.uploadButton.disabled = true;
|
||||||
|
this.uploadButton.innerHTML = '⏳ <span data-i18n="audio.notification.uploading">上傳中...</span>';
|
||||||
|
|
||||||
|
this.audioManager.addCustomAudio(audioName.trim(), file)
|
||||||
|
.then(audioData => {
|
||||||
|
this.showSuccess(this.t('audio.notification.uploadSuccess', '音效上傳成功: ') + audioData.name);
|
||||||
|
this.refreshAudioSelect();
|
||||||
|
this.refreshCustomAudioList();
|
||||||
|
// 清空檔案輸入
|
||||||
|
this.uploadInput.value = '';
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('❌ 上傳音效失敗:', error);
|
||||||
|
this.showError(error.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// 恢復按鈕狀態
|
||||||
|
this.uploadButton.disabled = false;
|
||||||
|
this.uploadButton.innerHTML = '📁 <span data-i18n="audio.notification.chooseFile">選擇檔案</span>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 處理刪除自訂音效
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.handleDeleteCustomAudio = function(audioId) {
|
||||||
|
const audioData = this.audioManager.getAudioById(audioId);
|
||||||
|
if (!audioData) return;
|
||||||
|
|
||||||
|
const confirmMessage = this.t('audio.notification.deleteConfirm', '確定要刪除音效 "{name}" 嗎?')
|
||||||
|
.replace('{name}', audioData.name);
|
||||||
|
|
||||||
|
if (!confirm(confirmMessage)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.audioManager.removeCustomAudio(audioId);
|
||||||
|
this.showSuccess(this.t('audio.notification.deleteSuccess', '音效已刪除'));
|
||||||
|
this.refreshAudioSelect();
|
||||||
|
this.refreshCustomAudioList();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 刪除音效失敗:', error);
|
||||||
|
this.showError(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新整個 UI
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.refreshUI = function() {
|
||||||
|
const settings = this.audioManager.getSettings();
|
||||||
|
|
||||||
|
// 更新啟用狀態
|
||||||
|
if (this.enabledToggle) {
|
||||||
|
this.enabledToggle.checked = settings.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新音量
|
||||||
|
if (this.volumeSlider && this.volumeValue) {
|
||||||
|
this.volumeSlider.value = settings.volume;
|
||||||
|
this.volumeValue.textContent = settings.volume + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新音效選擇
|
||||||
|
this.refreshAudioSelect();
|
||||||
|
|
||||||
|
// 更新自訂音效列表
|
||||||
|
this.refreshCustomAudioList();
|
||||||
|
|
||||||
|
// 更新控制項狀態
|
||||||
|
this.updateControlsState();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新音效選擇下拉選單
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.refreshAudioSelect = function() {
|
||||||
|
if (!this.audioSelect) return;
|
||||||
|
|
||||||
|
const settings = this.audioManager.getSettings();
|
||||||
|
const allAudios = this.audioManager.getAllAudios();
|
||||||
|
|
||||||
|
// 清空現有選項
|
||||||
|
this.audioSelect.innerHTML = '';
|
||||||
|
|
||||||
|
// 新增音效選項
|
||||||
|
allAudios.forEach(audio => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = audio.id;
|
||||||
|
|
||||||
|
// 使用翻譯後的名稱
|
||||||
|
let displayName = audio.name;
|
||||||
|
if (audio.isDefault) {
|
||||||
|
// 為預設音效提供翻譯
|
||||||
|
const translationKey = this.getDefaultAudioTranslationKey(audio.id);
|
||||||
|
if (translationKey) {
|
||||||
|
displayName = this.t(translationKey, audio.name);
|
||||||
|
}
|
||||||
|
displayName += ' (' + this.t('audio.notification.default', '預設') + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
option.textContent = displayName;
|
||||||
|
|
||||||
|
// 為預設音效選項新增 data-i18n 屬性,以便語言切換時自動更新
|
||||||
|
if (audio.isDefault) {
|
||||||
|
const translationKey = this.getDefaultAudioTranslationKey(audio.id);
|
||||||
|
if (translationKey) {
|
||||||
|
option.setAttribute('data-audio-id', audio.id);
|
||||||
|
option.setAttribute('data-is-default', 'true');
|
||||||
|
option.setAttribute('data-translation-key', translationKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audio.id === settings.selectedAudioId) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
this.audioSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新自訂音效列表
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.refreshCustomAudioList = function() {
|
||||||
|
if (!this.audioList) return;
|
||||||
|
|
||||||
|
const customAudios = this.audioManager.getSettings().customAudios;
|
||||||
|
|
||||||
|
if (customAudios.length === 0) {
|
||||||
|
this.audioList.innerHTML = `
|
||||||
|
<div class="audio-empty-state">
|
||||||
|
<div style="font-size: 32px; margin-bottom: 8px;">🎵</div>
|
||||||
|
<div data-i18n="audio.notification.noCustomAudios">尚未上傳任何自訂音效</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
customAudios.forEach(audio => {
|
||||||
|
html += this.createCustomAudioItemHTML(audio);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.audioList.innerHTML = html;
|
||||||
|
this.setupCustomAudioEvents();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 創建自訂音效項目 HTML
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.createCustomAudioItemHTML = function(audio) {
|
||||||
|
const createdDate = new Date(audio.createdAt).toLocaleDateString();
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="audio-custom-item" data-audio-id="${audio.id}">
|
||||||
|
<div class="audio-custom-info">
|
||||||
|
<div class="audio-custom-name">${Utils.escapeHtml(audio.name)}</div>
|
||||||
|
<div class="audio-custom-meta">
|
||||||
|
<span data-i18n="audio.notification.created">建立於</span>: ${createdDate}
|
||||||
|
| <span data-i18n="audio.notification.format">格式</span>: ${audio.mimeType}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="audio-custom-actions">
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary audio-play-btn"
|
||||||
|
data-audio-id="${audio.id}" title="播放">
|
||||||
|
▶️
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger audio-delete-btn"
|
||||||
|
data-audio-id="${audio.id}" title="刪除">
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 設置自訂音效項目事件
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.setupCustomAudioEvents = function() {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
// 播放按鈕事件
|
||||||
|
const playButtons = this.audioList.querySelectorAll('.audio-play-btn');
|
||||||
|
playButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const audioId = button.getAttribute('data-audio-id');
|
||||||
|
const audioData = self.audioManager.getAudioById(audioId);
|
||||||
|
if (audioData) {
|
||||||
|
self.audioManager.playAudio(audioData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 刪除按鈕事件
|
||||||
|
const deleteButtons = this.audioList.querySelectorAll('.audio-delete-btn');
|
||||||
|
deleteButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const audioId = button.getAttribute('data-audio-id');
|
||||||
|
self.handleDeleteCustomAudio(audioId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新控制項狀態
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.updateControlsState = function() {
|
||||||
|
const enabled = this.enabledToggle ? this.enabledToggle.checked : false;
|
||||||
|
|
||||||
|
// 根據啟用狀態禁用/啟用控制項
|
||||||
|
const controls = [
|
||||||
|
this.volumeSlider,
|
||||||
|
this.audioSelect,
|
||||||
|
this.testButton,
|
||||||
|
this.uploadButton
|
||||||
|
];
|
||||||
|
|
||||||
|
controls.forEach(control => {
|
||||||
|
if (control) {
|
||||||
|
control.disabled = !enabled;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顯示成功訊息
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.showSuccess = function(message) {
|
||||||
|
if (Utils && Utils.showMessage) {
|
||||||
|
Utils.showMessage(message, Utils.CONSTANTS.MESSAGE_SUCCESS);
|
||||||
|
} else {
|
||||||
|
console.log('✅', message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顯示錯誤訊息
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.showError = function(message) {
|
||||||
|
if (Utils && Utils.showMessage) {
|
||||||
|
Utils.showMessage(message, Utils.CONSTANTS.MESSAGE_ERROR);
|
||||||
|
} else {
|
||||||
|
console.error('❌', message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顯示音效名稱輸入模態框
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.showAudioNameModal = function(defaultName, onConfirm) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
// 創建模態框 HTML
|
||||||
|
const modalHTML = `
|
||||||
|
<div class="audio-name-modal-overlay" id="audioNameModalOverlay">
|
||||||
|
<div class="audio-name-modal">
|
||||||
|
<div class="audio-name-modal-header">
|
||||||
|
<h4 data-i18n="audio.notification.enterAudioName">輸入音效名稱</h4>
|
||||||
|
<button type="button" class="audio-name-modal-close" id="audioNameModalClose">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="audio-name-modal-body">
|
||||||
|
<label for="audioNameInput" data-i18n="audio.notification.audioName">音效名稱:</label>
|
||||||
|
<input type="text" id="audioNameInput" class="audio-name-input"
|
||||||
|
value="${Utils.escapeHtml(defaultName)}"
|
||||||
|
placeholder="${this.t('audio.notification.audioNamePlaceholder', '請輸入音效名稱...')}"
|
||||||
|
maxlength="50">
|
||||||
|
<div class="audio-name-hint" data-i18n="audio.notification.audioNameHint">
|
||||||
|
留空將使用預設檔案名稱
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="audio-name-modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" id="audioNameModalCancel">
|
||||||
|
<span data-i18n="buttons.cancel">取消</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="audioNameModalConfirm">
|
||||||
|
<span data-i18n="buttons.ok">確定</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 新增模態框到頁面
|
||||||
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||||
|
|
||||||
|
// 獲取元素引用
|
||||||
|
const overlay = document.getElementById('audioNameModalOverlay');
|
||||||
|
const input = document.getElementById('audioNameInput');
|
||||||
|
const closeBtn = document.getElementById('audioNameModalClose');
|
||||||
|
const cancelBtn = document.getElementById('audioNameModalCancel');
|
||||||
|
const confirmBtn = document.getElementById('audioNameModalConfirm');
|
||||||
|
|
||||||
|
// 聚焦輸入框並選中文字
|
||||||
|
setTimeout(() => {
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 關閉模態框函數
|
||||||
|
const closeModal = () => {
|
||||||
|
if (overlay && overlay.parentNode) {
|
||||||
|
overlay.parentNode.removeChild(overlay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 確認函數
|
||||||
|
const confirm = () => {
|
||||||
|
const audioName = input.value.trim() || defaultName;
|
||||||
|
closeModal();
|
||||||
|
if (onConfirm) {
|
||||||
|
onConfirm(audioName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 事件監聽器
|
||||||
|
closeBtn.addEventListener('click', closeModal);
|
||||||
|
cancelBtn.addEventListener('click', closeModal);
|
||||||
|
confirmBtn.addEventListener('click', confirm);
|
||||||
|
|
||||||
|
// 點擊遮罩關閉
|
||||||
|
overlay.addEventListener('click', function(e) {
|
||||||
|
if (e.target === overlay) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter 鍵確認,Escape 鍵取消
|
||||||
|
input.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
confirm();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新音效選擇器的翻譯
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.updateAudioSelectTranslations = function() {
|
||||||
|
if (!this.audioSelect) return;
|
||||||
|
|
||||||
|
const options = this.audioSelect.querySelectorAll('option[data-is-default="true"]');
|
||||||
|
options.forEach(option => {
|
||||||
|
const audioId = option.getAttribute('data-audio-id');
|
||||||
|
const translationKey = option.getAttribute('data-translation-key');
|
||||||
|
|
||||||
|
if (audioId && translationKey) {
|
||||||
|
const audioData = this.audioManager.getAudioById(audioId);
|
||||||
|
if (audioData) {
|
||||||
|
const translatedName = this.t(translationKey, audioData.name);
|
||||||
|
const defaultText = this.t('audio.notification.default', '預設');
|
||||||
|
option.textContent = translatedName + ' (' + defaultText + ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取預設音效的翻譯鍵值
|
||||||
|
*/
|
||||||
|
AudioSettingsUI.prototype.getDefaultAudioTranslationKey = function(audioId) {
|
||||||
|
const translationMap = {
|
||||||
|
'default-beep': 'audio.notification.defaultBeep',
|
||||||
|
'notification-ding': 'audio.notification.notificationDing',
|
||||||
|
'soft-chime': 'audio.notification.softChime'
|
||||||
|
};
|
||||||
|
return translationMap[audioId] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 匯出到全域命名空間
|
||||||
|
window.MCPFeedback.AudioSettingsUI = AudioSettingsUI;
|
||||||
|
|
||||||
|
})();
|
@ -30,7 +30,12 @@
|
|||||||
// 自動定時提交設定
|
// 自動定時提交設定
|
||||||
autoSubmitEnabled: false,
|
autoSubmitEnabled: false,
|
||||||
autoSubmitTimeout: 30,
|
autoSubmitTimeout: 30,
|
||||||
autoSubmitPromptId: null
|
autoSubmitPromptId: null,
|
||||||
|
// 音效通知設定
|
||||||
|
audioNotificationEnabled: false,
|
||||||
|
audioNotificationVolume: 50,
|
||||||
|
selectedAudioId: 'default-beep',
|
||||||
|
customAudios: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// 當前設定
|
// 當前設定
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
<link rel="stylesheet" href="/static/css/styles.css">
|
<link rel="stylesheet" href="/static/css/styles.css">
|
||||||
<link rel="stylesheet" href="/static/css/session-management.css">
|
<link rel="stylesheet" href="/static/css/session-management.css">
|
||||||
<link rel="stylesheet" href="/static/css/prompt-management.css">
|
<link rel="stylesheet" href="/static/css/prompt-management.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/audio-management.css">
|
||||||
<style>
|
<style>
|
||||||
/* 僅保留必要的頁面特定樣式和響應式調整 */
|
/* 僅保留必要的頁面特定樣式和響應式調整 */
|
||||||
|
|
||||||
@ -873,6 +874,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 音效通知設定卡片 -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<div class="settings-card-header">
|
||||||
|
<h3 class="settings-card-title" data-i18n="audio.notification.title">🔊 音效通知設定</h3>
|
||||||
|
</div>
|
||||||
|
<div class="settings-card-body" id="audioManagementContainer">
|
||||||
|
<!-- 音效管理 UI 將在這裡動態生成 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 提示詞管理卡片 -->
|
<!-- 提示詞管理卡片 -->
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<div class="settings-card-header">
|
<div class="settings-card-header">
|
||||||
@ -1018,6 +1029,10 @@
|
|||||||
<script src="/static/js/modules/prompt/prompt-settings-ui.js?v=2025010510"></script>
|
<script src="/static/js/modules/prompt/prompt-settings-ui.js?v=2025010510"></script>
|
||||||
<script src="/static/js/modules/prompt/prompt-input-buttons.js?v=2025010510"></script>
|
<script src="/static/js/modules/prompt/prompt-input-buttons.js?v=2025010510"></script>
|
||||||
|
|
||||||
|
<!-- 音效管理模組 -->
|
||||||
|
<script src="/static/js/modules/audio/audio-manager.js?v=2025010510"></script>
|
||||||
|
<script src="/static/js/modules/audio/audio-settings-ui.js?v=2025010510"></script>
|
||||||
|
|
||||||
<!-- 其他模組 -->
|
<!-- 其他模組 -->
|
||||||
<script src="/static/js/modules/utils.js?v=2025010510"></script>
|
<script src="/static/js/modules/utils.js?v=2025010510"></script>
|
||||||
<script src="/static/js/modules/tab-manager.js?v=2025010510"></script>
|
<script src="/static/js/modules/tab-manager.js?v=2025010510"></script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user