|
|
|
|
@@ -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>
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
<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>
|
|
|
|
|
</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">
|
|
|
|
|
<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">
|
|
|
|
|
<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)">
|
|
|
|
|
@@ -73,7 +69,10 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
<span class="form-hint">One extension per line or separated by commas. You can write <code>mp3</code> or <code>.mp3</code>.</span>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -127,6 +126,7 @@ const builtInMediaTypes = {
|
|
|
|
|
};
|
|
|
|
|
let sourceRoots = [];
|
|
|
|
|
let sourceConfig = {};
|
|
|
|
|
let allowedFilesMode = 'media_types';
|
|
|
|
|
|
|
|
|
|
function escapeHTML(value) {
|
|
|
|
|
return String(value || '').replace(/[&<>"']/g, (char) => ({
|
|
|
|
|
@@ -206,15 +206,6 @@ function collectSourcesForSave() {
|
|
|
|
|
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) {
|
|
|
|
|
if (!path || loadingNodes.has(path)) return;
|
|
|
|
|
loadingNodes.add(path);
|
|
|
|
|
@@ -262,21 +253,19 @@ function removeRoot(path) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function addSourceRoot() {
|
|
|
|
|
try {
|
|
|
|
|
const path = await pickFolder();
|
|
|
|
|
if (!path) return;
|
|
|
|
|
if (sourceRoots.some((root) => isSamePath(root, path))) {
|
|
|
|
|
toast('This source folder is already added', 'error');
|
|
|
|
|
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');
|
|
|
|
|
const input = document.getElementById('newSourcePath');
|
|
|
|
|
const path = (input?.value || '').trim();
|
|
|
|
|
if (!path) return;
|
|
|
|
|
if (sourceRoots.some((root) => isSamePath(root, path))) {
|
|
|
|
|
toast('This source folder is already added', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
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() {
|
|
|
|
|
@@ -410,9 +399,13 @@ function selectedMediaTypes() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateAllowedFilesModeUI() {
|
|
|
|
|
const mode = document.getElementById('allowedFilesMode').value || 'media_types';
|
|
|
|
|
document.getElementById('mediaTypesGroup').style.display = mode === 'media_types' ? '' : 'none';
|
|
|
|
|
document.getElementById('extensionsGroup').style.display = mode === 'extensions' ? '' : 'none';
|
|
|
|
|
document.getElementById('mediaTypesGroup').style.display = allowedFilesMode === 'media_types' ? '' : 'none';
|
|
|
|
|
document.getElementById('extensionsGroup').style.display = allowedFilesMode === 'extensions' ? '' : 'none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleAllowedFilesEditor() {
|
|
|
|
|
allowedFilesMode = allowedFilesMode === 'extensions' ? 'media_types' : 'extensions';
|
|
|
|
|
updateAllowedFilesModeUI();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderMediaTypeHints() {
|
|
|
|
|
@@ -429,9 +422,9 @@ async function loadSettings() {
|
|
|
|
|
document.getElementById('reserveGB').value = cfg.reserve_free_gb ?? 2;
|
|
|
|
|
document.getElementById('destFolder').value = cfg.dest_folder || 'media';
|
|
|
|
|
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('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('mediaTypeVideo').checked = (cfg.enabled_media_types || ['audio', 'video']).includes('video');
|
|
|
|
|
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,
|
|
|
|
|
dest_folder: document.getElementById('destFolder').value.trim() || 'media',
|
|
|
|
|
file_select_mode: document.getElementById('fileSelectMode').value,
|
|
|
|
|
allowed_files_mode: document.getElementById('allowedFilesMode').value,
|
|
|
|
|
allowed_files_mode: allowedFilesMode,
|
|
|
|
|
enabled_media_types: selectedMediaTypes(),
|
|
|
|
|
allowed_extensions: parseExtensionsInput(document.getElementById('allowedExtensions').value),
|
|
|
|
|
overwrite_mode: document.getElementById('overwriteMode').value,
|
|
|
|
|
|