feat (ui): selecteer meerdere bestanden tegelijkertijd
This commit is contained in:
+53
-9
@@ -17,6 +17,7 @@
|
|||||||
modalKnownFiles: {},
|
modalKnownFiles: {},
|
||||||
modalFileFilter: "",
|
modalFileFilter: "",
|
||||||
modalVisibleFiles: [],
|
modalVisibleFiles: [],
|
||||||
|
modalSelectionAnchorPath: null,
|
||||||
syncScrolling: false,
|
syncScrolling: false,
|
||||||
settings: {
|
settings: {
|
||||||
set_file_date_to_first_aired_date: false,
|
set_file_date_to_first_aired_date: false,
|
||||||
@@ -755,6 +756,7 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
state.modalFileFilter = "";
|
state.modalFileFilter = "";
|
||||||
|
state.modalSelectionAnchorPath = null;
|
||||||
el.fileModalTitle.textContent = "File Discovery";
|
el.fileModalTitle.textContent = "File Discovery";
|
||||||
el.modalAddSelectedFilesBtn.style.display = "";
|
el.modalAddSelectedFilesBtn.style.display = "";
|
||||||
if (el.modalFileFilterInput) {
|
if (el.modalFileFilterInput) {
|
||||||
@@ -819,22 +821,62 @@
|
|||||||
|
|
||||||
const isSelected = state.modalSelectedFilePaths.has(file.path);
|
const isSelected = state.modalSelectedFilePaths.has(file.path);
|
||||||
if (isSelected) li.classList.add("selected");
|
if (isSelected) li.classList.add("selected");
|
||||||
|
if (state.modalSelectionAnchorPath && state.modalSelectionAnchorPath === file.path) {
|
||||||
|
li.classList.add("modal-anchor");
|
||||||
|
}
|
||||||
|
|
||||||
li.addEventListener("click", () => {
|
li.addEventListener("click", (event) => {
|
||||||
if (state.modalSelectedFilePaths.has(file.path)) {
|
handleModalFileRowClick(file.path, event);
|
||||||
state.modalSelectedFilePaths.delete(file.path);
|
|
||||||
li.classList.remove("selected");
|
|
||||||
} else {
|
|
||||||
state.modalSelectedFilePaths.add(file.path);
|
|
||||||
li.classList.add("selected");
|
|
||||||
}
|
|
||||||
updateModalSelectionCount();
|
|
||||||
});
|
});
|
||||||
el.modalFilesList.appendChild(li);
|
el.modalFilesList.appendChild(li);
|
||||||
});
|
});
|
||||||
updateModalSelectionCount();
|
updateModalSelectionCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleModalFileRowClick(path, event) {
|
||||||
|
const clickedPath = (path || "").toString().trim();
|
||||||
|
if (!clickedPath) return;
|
||||||
|
|
||||||
|
const visiblePaths = (state.modalVisibleFiles || [])
|
||||||
|
.map((f) => (f && f.path ? f.path : ""))
|
||||||
|
.filter((p) => p);
|
||||||
|
const isShift = !!(event && event.shiftKey);
|
||||||
|
const isToggle = !!(event && (event.ctrlKey || event.metaKey));
|
||||||
|
|
||||||
|
if (isShift) {
|
||||||
|
const anchor = state.modalSelectionAnchorPath || "";
|
||||||
|
const from = visiblePaths.indexOf(anchor);
|
||||||
|
const to = visiblePaths.indexOf(clickedPath);
|
||||||
|
if (from >= 0 && to >= 0) {
|
||||||
|
const start = Math.min(from, to);
|
||||||
|
const end = Math.max(from, to);
|
||||||
|
const rangePaths = visiblePaths.slice(start, end + 1);
|
||||||
|
state.modalSelectedFilePaths = new Set(rangePaths);
|
||||||
|
} else {
|
||||||
|
// Fallback: no valid anchor in current visible list.
|
||||||
|
state.modalSelectedFilePaths = new Set([clickedPath]);
|
||||||
|
}
|
||||||
|
state.modalSelectionAnchorPath = clickedPath;
|
||||||
|
renderModalFiles();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isToggle) {
|
||||||
|
if (state.modalSelectedFilePaths.has(clickedPath)) {
|
||||||
|
state.modalSelectedFilePaths.delete(clickedPath);
|
||||||
|
} else {
|
||||||
|
state.modalSelectedFilePaths.add(clickedPath);
|
||||||
|
}
|
||||||
|
state.modalSelectionAnchorPath = clickedPath;
|
||||||
|
renderModalFiles();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.modalSelectedFilePaths = new Set([clickedPath]);
|
||||||
|
state.modalSelectionAnchorPath = clickedPath;
|
||||||
|
renderModalFiles();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadModalFiles(subpath) {
|
async function loadModalFiles(subpath) {
|
||||||
const rootId = el.modalRootSelect.value;
|
const rootId = el.modalRootSelect.value;
|
||||||
const chosenSubpath = (subpath || "").trim();
|
const chosenSubpath = (subpath || "").trim();
|
||||||
@@ -845,6 +887,7 @@
|
|||||||
const data = await api(
|
const data = await api(
|
||||||
`/api/files/discover?root_id=${encodeURIComponent(rootId)}&subpath=${encodeURIComponent(chosenSubpath)}&recursive=${recursive}&limit=200`
|
`/api/files/discover?root_id=${encodeURIComponent(rootId)}&subpath=${encodeURIComponent(chosenSubpath)}&recursive=${recursive}&limit=200`
|
||||||
);
|
);
|
||||||
|
state.modalSelectionAnchorPath = null;
|
||||||
state.modalFiles = data.items || [];
|
state.modalFiles = data.items || [];
|
||||||
state.modalFiles.forEach((file) => {
|
state.modalFiles.forEach((file) => {
|
||||||
const path = file.path || "";
|
const path = file.path || "";
|
||||||
@@ -891,6 +934,7 @@
|
|||||||
|
|
||||||
function clearModalSelection() {
|
function clearModalSelection() {
|
||||||
state.modalSelectedFilePaths.clear();
|
state.modalSelectedFilePaths.clear();
|
||||||
|
state.modalSelectionAnchorPath = null;
|
||||||
renderModalFiles();
|
renderModalFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -506,6 +506,10 @@ button.secondary {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#modalFilesList li.modal-anchor {
|
||||||
|
box-shadow: inset 0 0 0 1px var(--button-primary-border);
|
||||||
|
}
|
||||||
|
|
||||||
.modal-files-tools {
|
.modal-files-tools {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE_URL="${BASE_URL:-http://127.0.0.1:8085}"
|
||||||
|
ALT_BASE_URL="http://host.containers.internal:8085"
|
||||||
|
|
||||||
|
detect_base_url() {
|
||||||
|
if curl -fsS --max-time 2 "$BASE_URL/api/health" >/dev/null 2>&1; then
|
||||||
|
echo "$BASE_URL"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if curl -fsS --max-time 2 "$ALT_BASE_URL/api/health" >/dev/null 2>&1; then
|
||||||
|
echo "$ALT_BASE_URL"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "$BASE_URL"
|
||||||
|
}
|
||||||
|
|
||||||
|
BASE_URL="$(detect_base_url)"
|
||||||
|
echo "Using BASE_URL=$BASE_URL"
|
||||||
|
|
||||||
|
echo "== Feature test 1: modal selection modifiers are implemented =="
|
||||||
|
grep -q "modalSelectionAnchorPath" app/static/app.js || { echo "anchor state missing"; exit 1; }
|
||||||
|
grep -q "event.shiftKey" app/static/app.js || { echo "shift handling missing"; exit 1; }
|
||||||
|
grep -q "event.ctrlKey || event.metaKey" app/static/app.js || { echo "ctrl/meta handling missing"; exit 1; }
|
||||||
|
echo "modifier selection validation passed"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== Feature test 2: anchor reset on clear and folder reload =="
|
||||||
|
grep -q "state.modalSelectionAnchorPath = null;" app/static/app.js || { echo "anchor reset missing"; exit 1; }
|
||||||
|
grep -q "function clearModalSelection()" app/static/app.js || { echo "clearModalSelection missing"; exit 1; }
|
||||||
|
grep -q "async function loadModalFiles(subpath)" app/static/app.js || { echo "loadModalFiles missing"; exit 1; }
|
||||||
|
echo "anchor reset validation passed"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== Feature test 3: modal file list UI still present with batch actions =="
|
||||||
|
page="$(curl -fsS "$BASE_URL/")"
|
||||||
|
echo "$page" | grep -q 'id="modalFilesList"' || { echo "modalFilesList missing"; exit 1; }
|
||||||
|
echo "$page" | grep -q 'id="modalSelectAllFilesBtn"' || { echo "Select All button missing"; exit 1; }
|
||||||
|
echo "$page" | grep -q 'id="modalClearSelectionBtn"' || { echo "Clear Selection button missing"; exit 1; }
|
||||||
|
echo "modal UI validation passed"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "All file modal selection feature tests passed."
|
||||||
Reference in New Issue
Block a user