Compare commits
2 Commits
9fd02fb5bf
...
v1.6
| Author | SHA1 | Date | |
|---|---|---|---|
| e885e49647 | |||
| 70d301f78f |
@@ -5,7 +5,6 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="path-input-row">
|
<div class="path-input-row">
|
||||||
<input class="form-input" type="text" id="mountPath" placeholder="/Volumes/JUKEBOX or E:\\">
|
<input class="form-input" type="text" id="mountPath" placeholder="/Volumes/JUKEBOX or E:\\">
|
||||||
<button type="button" class="button-primary" onclick="pickMountPath()">+</button>
|
|
||||||
<button type="button" class="button-secondary" onclick="refreshSelectedDisk()">Refresh</button>
|
<button type="button" class="button-secondary" onclick="refreshSelectedDisk()">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-hint">Choose the directory where the removable disk is mounted. The app works with one selected disk at a time in standalone mode.</div>
|
<div class="form-hint">Choose the directory where the removable disk is mounted. The app works with one selected disk at a time in standalone mode.</div>
|
||||||
@@ -131,6 +130,7 @@ function renderDisk() {
|
|||||||
<button class="button-danger" data-action="start-copy" data-mode="replace" ${activeTask ? 'disabled' : ''}>Replace media</button>
|
<button class="button-danger" data-action="start-copy" data-mode="replace" ${activeTask ? 'disabled' : ''}>Replace media</button>
|
||||||
<button class="button-primary" data-action="start-copy" data-mode="add" ${activeTask ? 'disabled' : ''}>Add media</button>
|
<button class="button-primary" data-action="start-copy" data-mode="add" ${activeTask ? 'disabled' : ''}>Add media</button>
|
||||||
<button class="button-danger ${activeTask ? '' : 'hidden'}" data-action="cancel-copy">Cancel</button>
|
<button class="button-danger ${activeTask ? '' : 'hidden'}" data-action="cancel-copy">Cancel</button>
|
||||||
|
<button class="button-secondary" data-action="disk-settings" style="margin-left:auto">⚙ Settings</button>
|
||||||
` : ''}
|
` : ''}
|
||||||
${isForeign ? `
|
${isForeign ? `
|
||||||
<button class="button-secondary" data-action="init-disk">Initialize disk</button>
|
<button class="button-secondary" data-action="init-disk">Initialize disk</button>
|
||||||
@@ -193,7 +193,7 @@ function renderProfile(disk) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<section class="panel" id="profilePanel">
|
<section class="panel" id="profilePanel" style="display:none">
|
||||||
<h2>Профиль диска</h2>
|
<h2>Профиль диска</h2>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<h3>Параметры копирования</h3>
|
<h3>Параметры копирования</h3>
|
||||||
@@ -290,27 +290,6 @@ function startTaskPoll(taskID) {
|
|||||||
pollTask(taskID);
|
pollTask(taskID);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pickFolder() {
|
|
||||||
const response = await fetch('/api/system/pick-folder', { method: 'POST' });
|
|
||||||
const payload = await response.json();
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(payload.error || 'Failed to choose folder');
|
|
||||||
}
|
|
||||||
return payload.path || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pickMountPath() {
|
|
||||||
try {
|
|
||||||
const path = await pickFolder();
|
|
||||||
if (!path) return;
|
|
||||||
document.getElementById('mountPath').value = path;
|
|
||||||
localStorage.setItem('jukebox.selectedMountPath', path);
|
|
||||||
await refreshSelectedDisk();
|
|
||||||
} catch (error) {
|
|
||||||
toast(error.message || 'Failed to choose folder', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshSelectedDisk() {
|
async function refreshSelectedDisk() {
|
||||||
const mountPath = document.getElementById('mountPath').value.trim();
|
const mountPath = document.getElementById('mountPath').value.trim();
|
||||||
if (!mountPath) {
|
if (!mountPath) {
|
||||||
@@ -428,6 +407,13 @@ document.getElementById('diskState').addEventListener('click', (event) => {
|
|||||||
if (action === 'start-copy') startCopy(button.dataset.mode || 'add');
|
if (action === 'start-copy') startCopy(button.dataset.mode || 'add');
|
||||||
if (action === 'cancel-copy') cancelCopy();
|
if (action === 'cancel-copy') cancelCopy();
|
||||||
if (action === 'init-disk') initDisk();
|
if (action === 'init-disk') initDisk();
|
||||||
|
if (action === 'disk-settings') {
|
||||||
|
const panel = document.getElementById('profilePanel');
|
||||||
|
if (!panel) return;
|
||||||
|
const open = panel.style.display !== 'none';
|
||||||
|
panel.style.display = open ? 'none' : '';
|
||||||
|
button.textContent = open ? '⚙ Settings' : '⚙ Settings ✕';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const savedMountPath = localStorage.getItem('jukebox.selectedMountPath');
|
const savedMountPath = localStorage.getItem('jukebox.selectedMountPath');
|
||||||
|
|||||||
+32
-39
@@ -7,7 +7,8 @@
|
|||||||
<div class="form-hint">Add one or more root folders with source files. After that, expand each root and enable or disable individual nested folders with checkboxes.</div>
|
<div class="form-hint">Add one or more root folders with source files. After that, expand each root and enable or disable individual nested folders with checkboxes.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button type="button" class="button-primary" onclick="addSourceRoot()">Add source folder</button>
|
<input class="form-input" type="text" id="newSourcePath" placeholder="/media/movies">
|
||||||
|
<button type="button" class="button-primary" onclick="addSourceRoot()">Add</button>
|
||||||
<button type="button" class="button-secondary button-sm" onclick="reloadAllSourceTrees()">Refresh trees</button>
|
<button type="button" class="button-secondary button-sm" onclick="reloadAllSourceTrees()">Refresh trees</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="source-list">
|
<div class="source-list">
|
||||||
@@ -36,16 +37,11 @@
|
|||||||
<span class="form-hint">The new-only mode skips files already copied to this disk, even if they were later removed.</span>
|
<span class="form-hint">The new-only mode skips files already copied to this disk, even if they were later removed.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label" for="allowedFilesMode">Allowed file types</label>
|
|
||||||
<select class="form-select" id="allowedFilesMode" style="width:auto;max-width:420px" onchange="updateAllowedFilesModeUI()">
|
|
||||||
<option value="media_types">Audio, video, photo</option>
|
|
||||||
<option value="extensions">Custom extensions list</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" id="mediaTypesGroup">
|
<div class="form-group" id="mediaTypesGroup">
|
||||||
<label class="form-label">Built-in media types</label>
|
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">
|
||||||
|
<label class="form-label" style="margin:0">Allowed file types</label>
|
||||||
|
<button type="button" class="button-secondary button-sm" id="editAllowedFilesButton" onclick="toggleAllowedFilesEditor()">Edit list</button>
|
||||||
|
</div>
|
||||||
<div style="display:grid;gap:8px">
|
<div style="display:grid;gap:8px">
|
||||||
<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
|
<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
|
||||||
<input type="checkbox" id="mediaTypeAudio" style="width:15px;height:15px;accent-color:var(--accent)">
|
<input type="checkbox" id="mediaTypeAudio" style="width:15px;height:15px;accent-color:var(--accent)">
|
||||||
@@ -73,7 +69,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="extensionsGroup" style="display:none">
|
<div class="form-group" id="extensionsGroup" style="display:none">
|
||||||
<label class="form-label" for="allowedExtensions">Allowed extensions</label>
|
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">
|
||||||
|
<label class="form-label" for="allowedExtensions" style="margin:0">Allowed extensions</label>
|
||||||
|
<button type="button" class="button-secondary button-sm" onclick="toggleAllowedFilesEditor()">Use media types</button>
|
||||||
|
</div>
|
||||||
<textarea class="form-input" id="allowedExtensions" rows="5" placeholder=".mp3, .flac, .mp4"></textarea>
|
<textarea class="form-input" id="allowedExtensions" rows="5" placeholder=".mp3, .flac, .mp4"></textarea>
|
||||||
<span class="form-hint">One extension per line or separated by commas. You can write <code>mp3</code> or <code>.mp3</code>.</span>
|
<span class="form-hint">One extension per line or separated by commas. You can write <code>mp3</code> or <code>.mp3</code>.</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,6 +126,7 @@ const builtInMediaTypes = {
|
|||||||
};
|
};
|
||||||
let sourceRoots = [];
|
let sourceRoots = [];
|
||||||
let sourceConfig = {};
|
let sourceConfig = {};
|
||||||
|
let allowedFilesMode = 'media_types';
|
||||||
|
|
||||||
function escapeHTML(value) {
|
function escapeHTML(value) {
|
||||||
return String(value || '').replace(/[&<>"']/g, (char) => ({
|
return String(value || '').replace(/[&<>"']/g, (char) => ({
|
||||||
@@ -206,15 +206,6 @@ function collectSourcesForSave() {
|
|||||||
return items.sort((a, b) => a.path.localeCompare(b.path));
|
return items.sort((a, b) => a.path.localeCompare(b.path));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pickFolder() {
|
|
||||||
const response = await fetch('/api/system/pick-folder', { method: 'POST' });
|
|
||||||
const payload = await response.json();
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(payload.error || 'Failed to choose folder');
|
|
||||||
}
|
|
||||||
return payload.path || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadSourceChildren(path) {
|
async function loadSourceChildren(path) {
|
||||||
if (!path || loadingNodes.has(path)) return;
|
if (!path || loadingNodes.has(path)) return;
|
||||||
loadingNodes.add(path);
|
loadingNodes.add(path);
|
||||||
@@ -262,21 +253,19 @@ function removeRoot(path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function addSourceRoot() {
|
async function addSourceRoot() {
|
||||||
try {
|
const input = document.getElementById('newSourcePath');
|
||||||
const path = await pickFolder();
|
const path = (input?.value || '').trim();
|
||||||
if (!path) return;
|
if (!path) return;
|
||||||
if (sourceRoots.some((root) => isSamePath(root, path))) {
|
if (sourceRoots.some((root) => isSamePath(root, path))) {
|
||||||
toast('This source folder is already added', 'error');
|
toast('This source folder is already added', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
sourceRoots.push(path);
|
|
||||||
sourceRoots.sort((a, b) => a.localeCompare(b));
|
|
||||||
sourceConfig[path] = true;
|
|
||||||
expandedNodes.add(path);
|
|
||||||
await loadSourceChildren(path);
|
|
||||||
} catch (error) {
|
|
||||||
toast(error.message || 'Failed to choose folder', 'error');
|
|
||||||
}
|
}
|
||||||
|
sourceRoots.push(path);
|
||||||
|
sourceRoots.sort((a, b) => a.localeCompare(b));
|
||||||
|
sourceConfig[path] = true;
|
||||||
|
expandedNodes.add(path);
|
||||||
|
if (input) input.value = '';
|
||||||
|
await loadSourceChildren(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadAllSourceTrees() {
|
async function reloadAllSourceTrees() {
|
||||||
@@ -410,9 +399,13 @@ function selectedMediaTypes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateAllowedFilesModeUI() {
|
function updateAllowedFilesModeUI() {
|
||||||
const mode = document.getElementById('allowedFilesMode').value || 'media_types';
|
document.getElementById('mediaTypesGroup').style.display = allowedFilesMode === 'media_types' ? '' : 'none';
|
||||||
document.getElementById('mediaTypesGroup').style.display = mode === 'media_types' ? '' : 'none';
|
document.getElementById('extensionsGroup').style.display = allowedFilesMode === 'extensions' ? '' : 'none';
|
||||||
document.getElementById('extensionsGroup').style.display = mode === 'extensions' ? '' : 'none';
|
}
|
||||||
|
|
||||||
|
function toggleAllowedFilesEditor() {
|
||||||
|
allowedFilesMode = allowedFilesMode === 'extensions' ? 'media_types' : 'extensions';
|
||||||
|
updateAllowedFilesModeUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMediaTypeHints() {
|
function renderMediaTypeHints() {
|
||||||
@@ -429,9 +422,9 @@ async function loadSettings() {
|
|||||||
document.getElementById('reserveGB').value = cfg.reserve_free_gb ?? 2;
|
document.getElementById('reserveGB').value = cfg.reserve_free_gb ?? 2;
|
||||||
document.getElementById('destFolder').value = cfg.dest_folder || 'media';
|
document.getElementById('destFolder').value = cfg.dest_folder || 'media';
|
||||||
document.getElementById('fileSelectMode').value = cfg.file_select_mode || 'new';
|
document.getElementById('fileSelectMode').value = cfg.file_select_mode || 'new';
|
||||||
document.getElementById('allowedFilesMode').value = cfg.allowed_files_mode || 'media_types';
|
|
||||||
document.getElementById('overwriteMode').value = cfg.overwrite_mode || 'skip';
|
document.getElementById('overwriteMode').value = cfg.overwrite_mode || 'skip';
|
||||||
document.getElementById('autoCopy').checked = !!cfg.auto_copy;
|
document.getElementById('autoCopy').checked = !!cfg.auto_copy;
|
||||||
|
allowedFilesMode = cfg.allowed_files_mode || 'media_types';
|
||||||
document.getElementById('mediaTypeAudio').checked = (cfg.enabled_media_types || ['audio', 'video']).includes('audio');
|
document.getElementById('mediaTypeAudio').checked = (cfg.enabled_media_types || ['audio', 'video']).includes('audio');
|
||||||
document.getElementById('mediaTypeVideo').checked = (cfg.enabled_media_types || ['audio', 'video']).includes('video');
|
document.getElementById('mediaTypeVideo').checked = (cfg.enabled_media_types || ['audio', 'video']).includes('video');
|
||||||
document.getElementById('mediaTypePhoto').checked = (cfg.enabled_media_types || []).includes('photo');
|
document.getElementById('mediaTypePhoto').checked = (cfg.enabled_media_types || []).includes('photo');
|
||||||
@@ -456,7 +449,7 @@ async function saveSettings(event) {
|
|||||||
reserve_free_gb: parseFloat(document.getElementById('reserveGB').value) || 2,
|
reserve_free_gb: parseFloat(document.getElementById('reserveGB').value) || 2,
|
||||||
dest_folder: document.getElementById('destFolder').value.trim() || 'media',
|
dest_folder: document.getElementById('destFolder').value.trim() || 'media',
|
||||||
file_select_mode: document.getElementById('fileSelectMode').value,
|
file_select_mode: document.getElementById('fileSelectMode').value,
|
||||||
allowed_files_mode: document.getElementById('allowedFilesMode').value,
|
allowed_files_mode: allowedFilesMode,
|
||||||
enabled_media_types: selectedMediaTypes(),
|
enabled_media_types: selectedMediaTypes(),
|
||||||
allowed_extensions: parseExtensionsInput(document.getElementById('allowedExtensions').value),
|
allowed_extensions: parseExtensionsInput(document.getElementById('allowedExtensions').value),
|
||||||
overwrite_mode: document.getElementById('overwriteMode').value,
|
overwrite_mode: document.getElementById('overwriteMode').value,
|
||||||
|
|||||||
Reference in New Issue
Block a user