feat (ui): select files upgrade

This commit is contained in:
kodi
2026-03-09 14:13:59 +01:00
parent a22172f736
commit c5aaa20ce2
4 changed files with 170 additions and 74 deletions
+106 -52
View File
@@ -13,6 +13,9 @@
modalFolders: [], modalFolders: [],
modalFiles: [], modalFiles: [],
modalSelectedFilePaths: new Set(), modalSelectedFilePaths: new Set(),
modalKnownFiles: {},
modalFileFilter: "",
modalVisibleFiles: [],
syncScrolling: false, syncScrolling: false,
settings: { settings: {
set_file_date_to_first_aired_date: false, set_file_date_to_first_aired_date: false,
@@ -62,13 +65,13 @@
fileModalTitle: document.getElementById("fileModalTitle"), fileModalTitle: document.getElementById("fileModalTitle"),
closeFileModalBtn: document.getElementById("closeFileModalBtn"), closeFileModalBtn: document.getElementById("closeFileModalBtn"),
modalRootSelect: document.getElementById("modalRootSelect"), modalRootSelect: document.getElementById("modalRootSelect"),
modalFolderSelect: document.getElementById("modalFolderSelect"),
modalSubpathInput: document.getElementById("modalSubpathInput"),
modalRecursiveInput: document.getElementById("modalRecursiveInput"), modalRecursiveInput: document.getElementById("modalRecursiveInput"),
modalLoadFoldersBtn: document.getElementById("modalLoadFoldersBtn"), modalFileFilterInput: document.getElementById("modalFileFilterInput"),
modalLoadFilesBtn: document.getElementById("modalLoadFilesBtn"), modalSelectAllFilesBtn: document.getElementById("modalSelectAllFilesBtn"),
modalClearSelectionBtn: document.getElementById("modalClearSelectionBtn"),
modalFoldersList: document.getElementById("modalFoldersList"), modalFoldersList: document.getElementById("modalFoldersList"),
modalFilesList: document.getElementById("modalFilesList"), modalFilesList: document.getElementById("modalFilesList"),
modalSelectionCount: document.getElementById("modalSelectionCount"),
modalAddSelectedFilesBtn: document.getElementById("modalAddSelectedFilesBtn"), modalAddSelectedFilesBtn: document.getElementById("modalAddSelectedFilesBtn"),
}; };
@@ -361,7 +364,6 @@
populateDefaultRootOptions(); populateDefaultRootOptions();
if (state.roots.length) { if (state.roots.length) {
el.modalRootSelect.value = preferredRootId(); el.modalRootSelect.value = preferredRootId();
el.modalSubpathInput.value = "";
await loadModalFolders(); await loadModalFolders();
} }
} }
@@ -471,12 +473,28 @@
} }
function openFileModal() { function openFileModal() {
state.modalSelectedFilePaths = new Set(); const existingPaths = (state.selectedFiles || [])
.map((item) => (item.file && item.file.path) || "")
.filter((p) => typeof p === "string" && p.trim().length > 0);
state.modalSelectedFilePaths = new Set(existingPaths);
state.modalKnownFiles = {};
(state.selectedFiles || []).forEach((item) => {
const path = (item.file && item.file.path) || "";
if (!path) return;
state.modalKnownFiles[path] = {
path,
name: (item.file && item.file.name) || basename(path),
};
});
state.modalFileFilter = "";
el.fileModalTitle.textContent = "File Discovery"; el.fileModalTitle.textContent = "File Discovery";
el.modalAddSelectedFilesBtn.style.display = ""; el.modalAddSelectedFilesBtn.style.display = "";
el.modalLoadFilesBtn.style.display = ""; if (el.modalFileFilterInput) {
el.modalFileFilterInput.value = "";
}
el.fileModal.classList.remove("hidden"); el.fileModal.classList.remove("hidden");
el.fileModal.setAttribute("aria-hidden", "false"); el.fileModal.setAttribute("aria-hidden", "false");
updateModalSelectionCount();
} }
function closeFileModal() { function closeFileModal() {
@@ -487,62 +505,54 @@
async function loadModalFolders() { async function loadModalFolders() {
const rootId = el.modalRootSelect.value; const rootId = el.modalRootSelect.value;
if (!rootId) throw new Error("No root selected"); if (!rootId) throw new Error("No root selected");
const subpath = (el.modalSubpathInput.value || "").trim(); const subpath = "";
const data = await api( const data = await api(
`/api/files/folders?root_id=${encodeURIComponent(rootId)}&subpath=${encodeURIComponent(subpath)}&limit=5000` `/api/files/folders?root_id=${encodeURIComponent(rootId)}&subpath=${encodeURIComponent(subpath)}&limit=5000`
); );
state.modalFolders = data.items || []; state.modalFolders = data.items || [];
el.modalFolderSelect.innerHTML = "";
const placeholder = document.createElement("option");
placeholder.value = "";
placeholder.textContent = state.modalFolders.length ? "Choose folder..." : "(No folders)";
el.modalFolderSelect.appendChild(placeholder);
el.modalFoldersList.innerHTML = ""; el.modalFoldersList.innerHTML = "";
state.modalFolders.forEach((folder) => { state.modalFolders.forEach((folder) => {
const opt = document.createElement("option");
opt.value = folder.subpath;
opt.textContent = folder.subpath || folder.name;
el.modalFolderSelect.appendChild(opt);
const li = document.createElement("li"); const li = document.createElement("li");
const left = document.createElement("span"); const left = document.createElement("span");
left.textContent = folder.subpath || folder.name; left.textContent = folder.subpath || folder.name;
const right = document.createElement("div");
right.appendChild(makeBtn("Select", () => {
el.modalFolderSelect.value = folder.subpath;
el.modalSubpathInput.value = folder.subpath;
}, true));
li.appendChild(left); li.appendChild(left);
li.appendChild(right); li.addEventListener("click", () => withHandler(() => loadModalFiles(folder.subpath || ""), li));
el.modalFoldersList.appendChild(li); el.modalFoldersList.appendChild(li);
}); });
out("Folders loaded", data); out("Folders loaded", data);
} }
async function loadModalFiles() { function updateModalSelectionCount() {
const rootId = el.modalRootSelect.value; if (!el.modalSelectionCount) return;
const subpath = (el.modalFolderSelect.value || "").trim(); const count = state.modalSelectedFilePaths.size;
if (!rootId) throw new Error("No root selected"); const visibleCount = (state.modalVisibleFiles || []).length;
if (!subpath) throw new Error("Choose a folder first"); el.modalSelectionCount.textContent = `${count} selected (${visibleCount} visible)`;
if (el.modalAddSelectedFilesBtn) {
el.modalAddSelectedFilesBtn.disabled = count === 0;
}
}
function renderModalFiles() {
const filter = (state.modalFileFilter || "").toLowerCase();
const visible = (state.modalFiles || []).filter((file) => {
const text = `${file.relative_path || ""} ${file.name || ""}`.toLowerCase();
return !filter || text.includes(filter);
});
state.modalVisibleFiles = visible;
el.modalSubpathInput.value = subpath;
const recursive = el.modalRecursiveInput.checked ? "true" : "false";
const data = await api(
`/api/files/discover?root_id=${encodeURIComponent(rootId)}&subpath=${encodeURIComponent(subpath)}&recursive=${recursive}&limit=200`
);
state.modalFiles = data.items || [];
el.modalFilesList.innerHTML = ""; el.modalFilesList.innerHTML = "";
state.modalSelectedFilePaths = new Set(); visible.forEach((file) => {
state.modalFiles.forEach((file) => {
const li = document.createElement("li"); const li = document.createElement("li");
const left = document.createElement("span"); const left = document.createElement("span");
left.textContent = file.relative_path || file.name; left.textContent = file.relative_path || file.name;
const right = document.createElement("div"); li.appendChild(left);
right.appendChild(makeBtn("Toggle", () => {
const isSelected = state.modalSelectedFilePaths.has(file.path);
if (isSelected) li.classList.add("selected");
li.addEventListener("click", () => {
if (state.modalSelectedFilePaths.has(file.path)) { if (state.modalSelectedFilePaths.has(file.path)) {
state.modalSelectedFilePaths.delete(file.path); state.modalSelectedFilePaths.delete(file.path);
li.classList.remove("selected"); li.classList.remove("selected");
@@ -550,28 +560,72 @@
state.modalSelectedFilePaths.add(file.path); state.modalSelectedFilePaths.add(file.path);
li.classList.add("selected"); li.classList.add("selected");
} }
}, true)); updateModalSelectionCount();
li.appendChild(left); });
li.appendChild(right);
el.modalFilesList.appendChild(li); el.modalFilesList.appendChild(li);
}); });
updateModalSelectionCount();
}
async function loadModalFiles(subpath) {
const rootId = el.modalRootSelect.value;
const chosenSubpath = (subpath || "").trim();
if (!rootId) throw new Error("No root selected");
if (!chosenSubpath) throw new Error("Choose a folder first");
const recursive = el.modalRecursiveInput.checked ? "true" : "false";
const data = await api(
`/api/files/discover?root_id=${encodeURIComponent(rootId)}&subpath=${encodeURIComponent(chosenSubpath)}&recursive=${recursive}&limit=200`
);
state.modalFiles = data.items || [];
state.modalFiles.forEach((file) => {
const path = file.path || "";
if (!path) return;
state.modalKnownFiles[path] = {
path,
name: file.name || basename(path),
};
});
renderModalFiles();
out("Discovered files", data); out("Discovered files", data);
} }
async function addModalSelectedFiles() { async function addModalSelectedFiles() {
const selected = state.modalFiles.filter((f) => state.modalSelectedFilePaths.has(f.path)); const payload = Array.from(state.modalSelectedFilePaths)
if (!selected.length) throw new Error("Select at least one file"); .filter((path) => typeof path === "string" && path.trim().length > 0)
const payload = selected.map((f) => ({ path: f.path, name: f.name })); .map((path) => {
const known = state.modalKnownFiles[path] || {};
return {
path,
name: known.name || basename(path),
};
});
await api(q("/api/session/selected-files"), { method: "DELETE" });
if (payload.length) {
await api(q("/api/session/selected-files"), { await api(q("/api/session/selected-files"), {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items: payload }), body: JSON.stringify({ items: payload }),
}); });
}
await loadSelectedFiles(); await loadSelectedFiles();
closeFileModal(); closeFileModal();
} }
function selectAllVisibleModalFiles() {
(state.modalVisibleFiles || []).forEach((file) => {
state.modalSelectedFilePaths.add(file.path);
});
renderModalFiles();
}
function clearModalSelection() {
state.modalSelectedFilePaths.clear();
renderModalFiles();
}
async function reorderSelectedEpisodes(delta) { async function reorderSelectedEpisodes(delta) {
if (state.selectedPairIndex == null) throw new Error("Select an episode row first"); if (state.selectedPairIndex == null) throw new Error("Select an episode row first");
const from = state.selectedPairIndex; const from = state.selectedPairIndex;
@@ -693,12 +747,12 @@
if (e.target === el.fileModal) closeFileModal(); if (e.target === el.fileModal) closeFileModal();
}); });
el.modalRootSelect.addEventListener("change", () => withHandler(loadModalFolders, el.modalRootSelect)); el.modalRootSelect.addEventListener("change", () => withHandler(loadModalFolders, el.modalRootSelect));
el.modalLoadFoldersBtn.addEventListener("click", () => withHandler(loadModalFolders, el.modalLoadFoldersBtn)); el.modalFileFilterInput.addEventListener("input", () => {
el.modalLoadFilesBtn.addEventListener("click", () => withHandler(loadModalFiles, el.modalLoadFilesBtn)); state.modalFileFilter = el.modalFileFilterInput.value || "";
el.modalFolderSelect.addEventListener("change", () => { renderModalFiles();
const sub = (el.modalFolderSelect.value || "").trim();
if (sub) el.modalSubpathInput.value = sub;
}); });
el.modalSelectAllFilesBtn.addEventListener("click", selectAllVisibleModalFiles);
el.modalClearSelectionBtn.addEventListener("click", clearModalSelection);
el.modalAddSelectedFilesBtn.addEventListener("click", () => withHandler(addModalSelectedFiles, el.modalAddSelectedFilesBtn)); el.modalAddSelectedFilesBtn.addEventListener("click", () => withHandler(addModalSelectedFiles, el.modalAddSelectedFilesBtn));
el.selectedEpisodesList.addEventListener("scroll", () => syncScroll(el.selectedEpisodesList, el.selectedFilesList)); el.selectedEpisodesList.addEventListener("scroll", () => syncScroll(el.selectedEpisodesList, el.selectedFilesList));
+47 -4
View File
@@ -253,26 +253,69 @@ button.secondary {
} }
.modal-card { .modal-card {
width: min(1100px, 96vw); width: min(1400px, 90vw);
height: 80vh;
background: #ffffff; background: #ffffff;
border: 1px solid #d7dee9; border: 1px solid #d7dee9;
border-radius: 10px; border-radius: 10px;
padding: 12px; padding: 12px;
display: flex;
flex-direction: column;
min-height: 0;
} }
.modal-head { .modal-head {
display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 8px;
}
.modal-root-row {
margin-bottom: 8px;
} }
.modal-grid { .modal-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 12px; gap: 12px;
margin-top: 8px; margin-top: 4px;
flex: 1;
min-height: 0;
} }
.modal-grid .list { .modal-pane {
max-height: 280px; display: flex;
flex-direction: column;
min-height: 0;
}
.modal-pane .list {
flex: 1;
min-height: 0;
max-height: none;
}
#modalFoldersList li,
#modalFilesList li {
cursor: pointer;
}
.modal-files-tools {
margin-bottom: 8px;
}
.modal-files-tools input[type="text"] {
flex: 1;
min-width: 180px;
}
.modal-actions {
margin-top: 10px;
margin-bottom: 0;
justify-content: flex-end;
border-top: 1px solid #e4eaf2;
padding-top: 10px;
} }
.settings-card { .settings-card {
+12 -13
View File
@@ -104,36 +104,35 @@
<div id="fileModal" class="modal hidden" aria-hidden="true"> <div id="fileModal" class="modal hidden" aria-hidden="true">
<div class="modal-card"> <div class="modal-card">
<div class="row modal-head"> <div class="modal-head">
<h3 id="fileModalTitle">File Discovery</h3> <h3 id="fileModalTitle">File Discovery</h3>
<button id="closeFileModalBtn" class="secondary">Close</button>
</div> </div>
<div class="row"> <div class="row modal-root-row">
<label for="modalRootSelect">Root</label> <label for="modalRootSelect">Root</label>
<select id="modalRootSelect"></select> <select id="modalRootSelect"></select>
<label for="modalFolderSelect">Folder</label>
<select id="modalFolderSelect"></select>
</div>
<div class="row">
<input id="modalSubpathInput" type="text" placeholder="Current subpath (relative)" />
<button id="modalLoadFoldersBtn" class="secondary">Load Folders</button>
<label> <label>
<input id="modalRecursiveInput" type="checkbox" checked /> <input id="modalRecursiveInput" type="checkbox" checked />
recursive recursive
</label> </label>
<button id="modalLoadFilesBtn">Load Files</button>
</div> </div>
<div class="modal-grid"> <div class="modal-grid">
<div> <div class="modal-pane">
<h4>Folders</h4> <h4>Folders</h4>
<ul id="modalFoldersList" class="list"></ul> <ul id="modalFoldersList" class="list"></ul>
</div> </div>
<div> <div class="modal-pane">
<h4>Files</h4> <h4>Files</h4>
<div class="row modal-files-tools">
<input id="modalFileFilterInput" type="text" placeholder="Filter files..." />
<button id="modalSelectAllFilesBtn" class="secondary">Select All</button>
<button id="modalClearSelectionBtn" class="secondary">Clear Selection</button>
</div>
<ul id="modalFilesList" class="list"></ul> <ul id="modalFilesList" class="list"></ul>
</div> </div>
</div> </div>
<div class="row"> <div class="row modal-actions">
<span id="modalSelectionCount" class="muted"></span>
<button id="closeFileModalBtn" class="secondary">Close</button>
<button id="modalAddSelectedFilesBtn">Add Selected Files</button> <button id="modalAddSelectedFilesBtn">Add Selected Files</button>
</div> </div>
</div> </div>
Binary file not shown.