feat (ui): select files upgrade
This commit is contained in:
+111
-57
@@ -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 }));
|
||||
await api(q("/api/session/selected-files"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items: payload }),
|
||||
});
|
||||
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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user