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: [],
modalFiles: [],
modalSelectedFilePaths: new Set(),
modalKnownFiles: {},
modalFileFilter: "",
modalVisibleFiles: [],
syncScrolling: false,
settings: {
set_file_date_to_first_aired_date: false,
@@ -62,13 +65,13 @@
fileModalTitle: document.getElementById("fileModalTitle"),
closeFileModalBtn: document.getElementById("closeFileModalBtn"),
modalRootSelect: document.getElementById("modalRootSelect"),
modalFolderSelect: document.getElementById("modalFolderSelect"),
modalSubpathInput: document.getElementById("modalSubpathInput"),
modalRecursiveInput: document.getElementById("modalRecursiveInput"),
modalLoadFoldersBtn: document.getElementById("modalLoadFoldersBtn"),
modalLoadFilesBtn: document.getElementById("modalLoadFilesBtn"),
modalFileFilterInput: document.getElementById("modalFileFilterInput"),
modalSelectAllFilesBtn: document.getElementById("modalSelectAllFilesBtn"),
modalClearSelectionBtn: document.getElementById("modalClearSelectionBtn"),
modalFoldersList: document.getElementById("modalFoldersList"),
modalFilesList: document.getElementById("modalFilesList"),
modalSelectionCount: document.getElementById("modalSelectionCount"),
modalAddSelectedFilesBtn: document.getElementById("modalAddSelectedFilesBtn"),
};
@@ -361,7 +364,6 @@
populateDefaultRootOptions();
if (state.roots.length) {
el.modalRootSelect.value = preferredRootId();
el.modalSubpathInput.value = "";
await loadModalFolders();
}
}
@@ -471,12 +473,28 @@
}
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.modalAddSelectedFilesBtn.style.display = "";
el.modalLoadFilesBtn.style.display = "";
if (el.modalFileFilterInput) {
el.modalFileFilterInput.value = "";
}
el.fileModal.classList.remove("hidden");
el.fileModal.setAttribute("aria-hidden", "false");
updateModalSelectionCount();
}
function closeFileModal() {
@@ -487,62 +505,54 @@
async function loadModalFolders() {
const rootId = el.modalRootSelect.value;
if (!rootId) throw new Error("No root selected");
const subpath = (el.modalSubpathInput.value || "").trim();
const subpath = "";
const data = await api(
`/api/files/folders?root_id=${encodeURIComponent(rootId)}&subpath=${encodeURIComponent(subpath)}&limit=5000`
);
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 = "";
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 left = document.createElement("span");
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(right);
li.addEventListener("click", () => withHandler(() => loadModalFiles(folder.subpath || ""), li));
el.modalFoldersList.appendChild(li);
});
out("Folders loaded", data);
}
async function loadModalFiles() {
const rootId = el.modalRootSelect.value;
const subpath = (el.modalFolderSelect.value || "").trim();
if (!rootId) throw new Error("No root selected");
if (!subpath) throw new Error("Choose a folder first");
function updateModalSelectionCount() {
if (!el.modalSelectionCount) return;
const count = state.modalSelectedFilePaths.size;
const visibleCount = (state.modalVisibleFiles || []).length;
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 = "";
state.modalSelectedFilePaths = new Set();
state.modalFiles.forEach((file) => {
visible.forEach((file) => {
const li = document.createElement("li");
const left = document.createElement("span");
left.textContent = file.relative_path || file.name;
const right = document.createElement("div");
right.appendChild(makeBtn("Toggle", () => {
li.appendChild(left);
const isSelected = state.modalSelectedFilePaths.has(file.path);
if (isSelected) li.classList.add("selected");
li.addEventListener("click", () => {
if (state.modalSelectedFilePaths.has(file.path)) {
state.modalSelectedFilePaths.delete(file.path);
li.classList.remove("selected");
@@ -550,28 +560,72 @@
state.modalSelectedFilePaths.add(file.path);
li.classList.add("selected");
}
}, true));
li.appendChild(left);
li.appendChild(right);
updateModalSelectionCount();
});
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);
}
async function addModalSelectedFiles() {
const selected = state.modalFiles.filter((f) => state.modalSelectedFilePaths.has(f.path));
if (!selected.length) throw new Error("Select at least one file");
const payload = selected.map((f) => ({ path: f.path, name: f.name }));
const payload = Array.from(state.modalSelectedFilePaths)
.filter((path) => typeof path === "string" && path.trim().length > 0)
.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"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items: payload }),
});
}
await loadSelectedFiles();
closeFileModal();
}
function selectAllVisibleModalFiles() {
(state.modalVisibleFiles || []).forEach((file) => {
state.modalSelectedFilePaths.add(file.path);
});
renderModalFiles();
}
function clearModalSelection() {
state.modalSelectedFilePaths.clear();
renderModalFiles();
}
async function reorderSelectedEpisodes(delta) {
if (state.selectedPairIndex == null) throw new Error("Select an episode row first");
const from = state.selectedPairIndex;
@@ -693,12 +747,12 @@
if (e.target === el.fileModal) closeFileModal();
});
el.modalRootSelect.addEventListener("change", () => withHandler(loadModalFolders, el.modalRootSelect));
el.modalLoadFoldersBtn.addEventListener("click", () => withHandler(loadModalFolders, el.modalLoadFoldersBtn));
el.modalLoadFilesBtn.addEventListener("click", () => withHandler(loadModalFiles, el.modalLoadFilesBtn));
el.modalFolderSelect.addEventListener("change", () => {
const sub = (el.modalFolderSelect.value || "").trim();
if (sub) el.modalSubpathInput.value = sub;
el.modalFileFilterInput.addEventListener("input", () => {
state.modalFileFilter = el.modalFileFilterInput.value || "";
renderModalFiles();
});
el.modalSelectAllFilesBtn.addEventListener("click", selectAllVisibleModalFiles);
el.modalClearSelectionBtn.addEventListener("click", clearModalSelection);
el.modalAddSelectedFilesBtn.addEventListener("click", () => withHandler(addModalSelectedFiles, el.modalAddSelectedFilesBtn));
el.selectedEpisodesList.addEventListener("scroll", () => syncScroll(el.selectedEpisodesList, el.selectedFilesList));
+47 -4
View File
@@ -253,26 +253,69 @@ button.secondary {
}
.modal-card {
width: min(1100px, 96vw);
width: min(1400px, 90vw);
height: 80vh;
background: #ffffff;
border: 1px solid #d7dee9;
border-radius: 10px;
padding: 12px;
display: flex;
flex-direction: column;
min-height: 0;
}
.modal-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.modal-root-row {
margin-bottom: 8px;
}
.modal-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 8px;
margin-top: 4px;
flex: 1;
min-height: 0;
}
.modal-grid .list {
max-height: 280px;
.modal-pane {
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 {
+12 -13
View File
@@ -104,36 +104,35 @@
<div id="fileModal" class="modal hidden" aria-hidden="true">
<div class="modal-card">
<div class="row modal-head">
<div class="modal-head">
<h3 id="fileModalTitle">File Discovery</h3>
<button id="closeFileModalBtn" class="secondary">Close</button>
</div>
<div class="row">
<div class="row modal-root-row">
<label for="modalRootSelect">Root</label>
<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>
<input id="modalRecursiveInput" type="checkbox" checked />
recursive
</label>
<button id="modalLoadFilesBtn">Load Files</button>
</div>
<div class="modal-grid">
<div>
<div class="modal-pane">
<h4>Folders</h4>
<ul id="modalFoldersList" class="list"></ul>
</div>
<div>
<div class="modal-pane">
<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>
</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>
</div>
</div>
Binary file not shown.