feat: Add Line Audio Generation and Line List Manager
This commit is contained in:
@@ -533,10 +533,16 @@
|
||||
<div class="layout-wrapper">
|
||||
<div class="header">
|
||||
<h2>Broadcast Generator & Manager</h2>
|
||||
<a href="/" class="btn">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||
Back to Console
|
||||
</a>
|
||||
<div style="display: flex; gap: 12px; align-items: center;">
|
||||
<div style="display: flex; background: var(--bg-input); border: 1px solid var(--bg-border); border-radius: 6px; padding: 4px;">
|
||||
<button class="btn btn-primary" id="mode-station" onclick="switchMode('station')" style="border: none; padding: 6px 16px;">Station</button>
|
||||
<button class="btn" id="mode-line" onclick="switchMode('line')" style="border: none; padding: 6px 16px;">Line</button>
|
||||
</div>
|
||||
<a href="/" class="btn">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||
Back to Console
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
@@ -561,12 +567,12 @@
|
||||
<!-- Single Generation View -->
|
||||
<div id="view-single" style="display: flex; flex-direction: column; gap: 20px;">
|
||||
<div class="form-group">
|
||||
<label>Chinese Station Name (e.g., 北京)</label>
|
||||
<label id="lbl-zh-name">Chinese Station Name (e.g., 北京)</label>
|
||||
<input type="text" id="zh-text" placeholder="输入中文站名">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Japanese Station Name (e.g., ちば)</label>
|
||||
<label id="lbl-en-name">Japanese Station Name (e.g., ちば)</label>
|
||||
<input type="text" id="en-text" placeholder="输入日文站名">
|
||||
</div>
|
||||
|
||||
@@ -591,7 +597,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top:12px; padding-top:20px; border-top:1px solid var(--bg-border)">
|
||||
<label>Station Code (e.g., 01-01)</label>
|
||||
<label id="lbl-station-code">Station Code (e.g., 01-01)</label>
|
||||
<input type="text" id="station-code" placeholder="01-01">
|
||||
</div>
|
||||
|
||||
@@ -642,13 +648,13 @@
|
||||
<div class="panel">
|
||||
<div class="panel-title">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
|
||||
Manage list.txt
|
||||
<span id="manage-title">Manage list.txt</span>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items: flex-end; margin-bottom: 8px;">
|
||||
<div class="panel-subtitle" style="margin:0; border:none; padding:0;">Manage station audio sequences visually. (Supports multiple IDs separated by /)</div>
|
||||
<div class="panel-subtitle" id="manage-subtitle" style="margin:0; border:none; padding:0;">Manage station audio sequences visually. (Supports multiple IDs separated by /)</div>
|
||||
<div style="display:flex; gap:10px; flex-shrink: 0;">
|
||||
<button class="btn" onclick="openAddModal()" style="white-space: nowrap;">
|
||||
<button class="btn" id="btn-add-station" onclick="openAddModal()" style="white-space: nowrap;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||
Add Station
|
||||
</button>
|
||||
@@ -681,7 +687,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Station Code</label>
|
||||
<label id="lbl-edit-code">Station Code</label>
|
||||
<input type="text" id="edit-code" placeholder="e.g. 01-01 / 01-01/01-02">
|
||||
</div>
|
||||
|
||||
@@ -723,6 +729,7 @@
|
||||
const ZH_REFERENCE_ID = "0aee9f0f5683480d987c9e3c5d2de04c";
|
||||
const JA_REFERENCE_ID = "38985e545cff41b79c11c32b0196c9a0";
|
||||
|
||||
let currentMode = 'station'; // 'station' or 'line'
|
||||
let zhTempFile = null;
|
||||
let jaTempFile = null;
|
||||
|
||||
@@ -732,6 +739,37 @@
|
||||
let currentSequence = [];
|
||||
let selectedFileValue = '';
|
||||
|
||||
async function switchMode(mode) {
|
||||
currentMode = mode;
|
||||
const btnStation = document.getElementById('mode-station');
|
||||
const btnLine = document.getElementById('mode-line');
|
||||
|
||||
if (mode === 'station') {
|
||||
btnStation.className = 'btn btn-primary';
|
||||
btnLine.className = 'btn';
|
||||
document.getElementById('lbl-zh-name').textContent = 'Chinese Station Name (e.g., 北京)';
|
||||
document.getElementById('lbl-en-name').textContent = 'Japanese Station Name (e.g., ちば)';
|
||||
document.getElementById('lbl-station-code').textContent = 'Station Code (e.g., 01-01)';
|
||||
document.getElementById('lbl-edit-code').textContent = 'Station Code';
|
||||
document.getElementById('manage-title').textContent = 'Manage list.txt';
|
||||
document.getElementById('manage-subtitle').textContent = 'Manage station audio sequences visually. (Supports multiple IDs separated by /)';
|
||||
document.getElementById('btn-add-station').innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg> Add Station`;
|
||||
} else {
|
||||
btnStation.className = 'btn';
|
||||
btnLine.className = 'btn btn-primary';
|
||||
document.getElementById('lbl-zh-name').textContent = 'Chinese Line Name (e.g., 一号线)';
|
||||
document.getElementById('lbl-en-name').textContent = 'Japanese Line Name (e.g., 1ごうせん)';
|
||||
document.getElementById('lbl-station-code').textContent = 'Line Code (e.g., line1)';
|
||||
document.getElementById('lbl-edit-code').textContent = 'Line Code';
|
||||
document.getElementById('manage-title').textContent = 'Manage line.txt';
|
||||
document.getElementById('manage-subtitle').textContent = 'Manage line audio sequences visually.';
|
||||
document.getElementById('btn-add-station').innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg> Add Line`;
|
||||
}
|
||||
|
||||
document.getElementById('list-container').innerHTML = '<div style="text-align:center; padding: 40px; color:var(--text-muted)">Loading...</div>';
|
||||
await loadList();
|
||||
}
|
||||
|
||||
function toggleCustomSelect() {
|
||||
const trigger = document.querySelector('#custom-file-select .custom-select-trigger');
|
||||
const options = document.getElementById('custom-select-options');
|
||||
@@ -833,7 +871,7 @@
|
||||
const res = await fetch('/api/tts/import', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ stationCode: code, zhTemp, jaTemp })
|
||||
body: JSON.stringify({ stationCode: code, zhTemp, jaTemp, type: currentMode })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.message);
|
||||
@@ -934,7 +972,7 @@
|
||||
btn.disabled = true;
|
||||
btnCancel.style.display = 'none'; // Prevent cancel during generation
|
||||
statusBox.style.display = 'block';
|
||||
statusBox.innerHTML = `Starting batch generation for ${batchTasks.length} stations...\n`;
|
||||
statusBox.innerHTML = `Starting batch generation for ${batchTasks.length} ${currentMode === 'station' ? 'stations' : 'lines'}...\n`;
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
@@ -1070,7 +1108,7 @@
|
||||
if (importCount > 0) {
|
||||
renderList();
|
||||
saveList();
|
||||
showToast("Batch Complete", `${importCount} stations imported and added to list`);
|
||||
showToast("Batch Complete", `${importCount} ${currentMode === 'station' ? 'stations' : 'lines'} imported and added to list`);
|
||||
statusBox.innerHTML += `\n<b>All ${importCount} items imported successfully!</b>`;
|
||||
|
||||
// Reset UI after 2 seconds
|
||||
@@ -1153,14 +1191,14 @@
|
||||
showToast("Error", e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Generate Audio";
|
||||
btn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> Generate Audio`;
|
||||
}
|
||||
}
|
||||
|
||||
async function importAudio() {
|
||||
let code = document.getElementById('station-code').value.trim();
|
||||
if (!code) {
|
||||
showToast("Error", "Please enter a station code");
|
||||
showToast("Error", `Please enter a ${currentMode === 'station' ? 'station' : 'line'} code`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1186,14 +1224,15 @@
|
||||
body: JSON.stringify({
|
||||
stationCode: code,
|
||||
zhTemp: zhTempFile,
|
||||
jaTemp: jaTempFile
|
||||
jaTemp: jaTempFile,
|
||||
type: currentMode
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.message);
|
||||
|
||||
showToast("Success", "Files imported to Station folder!");
|
||||
showToast("Success", `Files imported to ${currentMode === 'station' ? 'Station' : 'General'} folder!`);
|
||||
|
||||
// Automatically add/update to listData if generated via this panel
|
||||
const filesStr = `ch-${code}.wav,ja-${code}.wav`;
|
||||
@@ -1219,7 +1258,7 @@
|
||||
showToast("Error", "Import failed: " + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Import to Station Folder & Add to List";
|
||||
btn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg> Import & Add to List`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1254,7 +1293,8 @@
|
||||
|
||||
async function loadList() {
|
||||
try {
|
||||
const res = await fetch('/api/files/content?path=list.txt');
|
||||
const targetPath = currentMode === 'station' ? 'list.txt' : 'Audio/general/line.txt';
|
||||
const res = await fetch(`/api/files/content?path=${encodeURIComponent(targetPath)}`);
|
||||
const data = await res.json();
|
||||
listData = [];
|
||||
if (data.success && data.content) {
|
||||
@@ -1270,7 +1310,7 @@
|
||||
}
|
||||
renderList();
|
||||
} catch (e) {
|
||||
document.getElementById('list-container').innerHTML = '<div style="color:var(--danger)">Failed to load list.txt</div>';
|
||||
document.getElementById('list-container').innerHTML = `<div style="color:var(--danger)">Failed to load ${currentMode === 'station' ? 'list.txt' : 'line.txt'}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1305,7 +1345,8 @@
|
||||
}
|
||||
|
||||
function removeListItem(index) {
|
||||
if (confirm(`Delete station ${listData[index].code}?`)) {
|
||||
const typeName = currentMode === 'station' ? 'station' : 'line';
|
||||
if (confirm(`Delete ${typeName} ${listData[index].code}?`)) {
|
||||
listData.splice(index, 1);
|
||||
renderList();
|
||||
}
|
||||
@@ -1315,7 +1356,7 @@
|
||||
function openAddModal() {
|
||||
editingIndex = -1;
|
||||
currentSequence = [];
|
||||
document.getElementById('modal-title').textContent = 'Add Station Configuration';
|
||||
document.getElementById('modal-title').textContent = currentMode === 'station' ? 'Add Station Configuration' : 'Add Line Configuration';
|
||||
document.getElementById('edit-code').value = '';
|
||||
renderSequence();
|
||||
selectCustomOption('', '-- Select an audio file --');
|
||||
@@ -1326,7 +1367,7 @@
|
||||
editingIndex = index;
|
||||
const item = listData[index];
|
||||
currentSequence = [...item.files];
|
||||
document.getElementById('modal-title').textContent = `Edit Station: ${item.code}`;
|
||||
document.getElementById('modal-title').textContent = currentMode === 'station' ? `Edit Station: ${item.code}` : `Edit Line: ${item.code}`;
|
||||
document.getElementById('edit-code').value = item.code;
|
||||
renderSequence();
|
||||
selectCustomOption('', '-- Select an audio file --');
|
||||
@@ -1386,14 +1427,15 @@
|
||||
|
||||
function confirmEdit() {
|
||||
const code = document.getElementById('edit-code').value.trim();
|
||||
const typeName = currentMode === 'station' ? 'Station' : 'Line';
|
||||
if (!code) {
|
||||
alert("Station code is required");
|
||||
alert(`${typeName} code is required`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingIndex === -1) {
|
||||
if (listData.some(item => item.code === code)) {
|
||||
alert(`Station ${code} already exists!`);
|
||||
alert(`${typeName} ${code} already exists!`);
|
||||
return;
|
||||
}
|
||||
listData.push({ code, files: [...currentSequence] });
|
||||
@@ -1417,17 +1459,18 @@
|
||||
.join('\n');
|
||||
|
||||
try {
|
||||
const targetPath = currentMode === 'station' ? 'list.txt' : 'Audio/general/line.txt';
|
||||
const res = await fetch('/api/files/content', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
path: 'list.txt',
|
||||
path: targetPath,
|
||||
content: content + '\n'
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
showToast("Success", "list.txt saved successfully!");
|
||||
showToast("Success", `${currentMode === 'station' ? 'list.txt' : 'line.txt'} saved successfully!`);
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
@@ -1435,7 +1478,7 @@
|
||||
showToast("Error", "Save failed: " + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Save to Server";
|
||||
btn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg> Save to Server`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user