aantal mappen en layout aangepast
This commit is contained in:
+1
-1
@@ -34,7 +34,7 @@ def discover_files(
|
|||||||
def discover_folders(
|
def discover_folders(
|
||||||
root_id: str = Query(..., min_length=1),
|
root_id: str = Query(..., min_length=1),
|
||||||
subpath: str = Query(""),
|
subpath: str = Query(""),
|
||||||
limit: int = Query(500, ge=1, le=2000),
|
limit: int = Query(5000, ge=1, le=20000),
|
||||||
):
|
):
|
||||||
service = FileDiscoveryService()
|
service = FileDiscoveryService()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class FileDiscoveryService:
|
|||||||
self,
|
self,
|
||||||
root_id: str,
|
root_id: str,
|
||||||
subpath: str = "",
|
subpath: str = "",
|
||||||
limit: int = 500,
|
limit: int = 5000,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
root = self._get_root_by_id(root_id)
|
root = self._get_root_by_id(root_id)
|
||||||
target = self._resolve_target(root["path"], subpath)
|
target = self._resolve_target(root["path"], subpath)
|
||||||
@@ -109,10 +109,12 @@ class FileDiscoveryService:
|
|||||||
break
|
break
|
||||||
if not entry.is_dir():
|
if not entry.is_dir():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
relative_to_root = entry.resolve().relative_to(root["path"])
|
relative_to_root = entry.resolve().relative_to(root["path"])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
folders.append(
|
folders.append(
|
||||||
{
|
{
|
||||||
"name": entry.name,
|
"name": entry.name,
|
||||||
|
|||||||
+299
-183
@@ -1,14 +1,18 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const STORAGE_KEY = "rename_mvp_session_id";
|
const STORAGE_KEY = "rename_mvp_session_id";
|
||||||
const sessionId = initSessionId();
|
|
||||||
const state = {
|
const state = {
|
||||||
sessionId,
|
sessionId: initSessionId(),
|
||||||
selectedSeries: null,
|
selectedSeries: null,
|
||||||
episodes: [],
|
episodes: [],
|
||||||
|
selectedEpisodes: [],
|
||||||
|
selectedFiles: [],
|
||||||
|
selectedPairIndex: null,
|
||||||
roots: [],
|
roots: [],
|
||||||
folders: [],
|
modalFolders: [],
|
||||||
discoveredFiles: [],
|
modalFiles: [],
|
||||||
currentSubpath: "",
|
modalSelectedFilePaths: new Set(),
|
||||||
|
syncScrolling: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const el = {
|
const el = {
|
||||||
@@ -20,23 +24,35 @@
|
|||||||
seriesInfo: document.getElementById("seriesInfo"),
|
seriesInfo: document.getElementById("seriesInfo"),
|
||||||
refreshEpisodesBtn: document.getElementById("refreshEpisodesBtn"),
|
refreshEpisodesBtn: document.getElementById("refreshEpisodesBtn"),
|
||||||
episodesList: document.getElementById("episodesList"),
|
episodesList: document.getElementById("episodesList"),
|
||||||
|
episodeMeta: document.getElementById("episodeMeta"),
|
||||||
refreshSelectedEpisodesBtn: document.getElementById("refreshSelectedEpisodesBtn"),
|
refreshSelectedEpisodesBtn: document.getElementById("refreshSelectedEpisodesBtn"),
|
||||||
clearSelectedEpisodesBtn: document.getElementById("clearSelectedEpisodesBtn"),
|
clearSelectedEpisodesBtn: document.getElementById("clearSelectedEpisodesBtn"),
|
||||||
|
episodeUpBtn: document.getElementById("episodeUpBtn"),
|
||||||
|
episodeDownBtn: document.getElementById("episodeDownBtn"),
|
||||||
|
episodeRemoveBtn: document.getElementById("episodeRemoveBtn"),
|
||||||
selectedEpisodesList: document.getElementById("selectedEpisodesList"),
|
selectedEpisodesList: document.getElementById("selectedEpisodesList"),
|
||||||
rootsSelect: document.getElementById("rootsSelect"),
|
fileMeta: document.getElementById("fileMeta"),
|
||||||
foldersSelect: document.getElementById("foldersSelect"),
|
selectFilesBtn: document.getElementById("selectFilesBtn"),
|
||||||
refreshRootsBtn: document.getElementById("refreshRootsBtn"),
|
|
||||||
subpathInput: document.getElementById("subpathInput"),
|
|
||||||
loadFoldersBtn: document.getElementById("loadFoldersBtn"),
|
|
||||||
loadFilesBtn: document.getElementById("loadFilesBtn"),
|
|
||||||
recursiveInput: document.getElementById("recursiveInput"),
|
|
||||||
discoveredFilesList: document.getElementById("discoveredFilesList"),
|
|
||||||
refreshSelectedFilesBtn: document.getElementById("refreshSelectedFilesBtn"),
|
refreshSelectedFilesBtn: document.getElementById("refreshSelectedFilesBtn"),
|
||||||
clearSelectedFilesBtn: document.getElementById("clearSelectedFilesBtn"),
|
clearSelectedFilesBtn: document.getElementById("clearSelectedFilesBtn"),
|
||||||
|
fileUpBtn: document.getElementById("fileUpBtn"),
|
||||||
|
fileDownBtn: document.getElementById("fileDownBtn"),
|
||||||
|
fileRemoveBtn: document.getElementById("fileRemoveBtn"),
|
||||||
selectedFilesList: document.getElementById("selectedFilesList"),
|
selectedFilesList: document.getElementById("selectedFilesList"),
|
||||||
mappingPreviewBtn: document.getElementById("mappingPreviewBtn"),
|
|
||||||
filenamePreviewBtn: document.getElementById("filenamePreviewBtn"),
|
filenamePreviewBtn: document.getElementById("filenamePreviewBtn"),
|
||||||
renameExecuteBtn: document.getElementById("renameExecuteBtn"),
|
renameExecuteBtn: document.getElementById("renameExecuteBtn"),
|
||||||
|
fileModal: document.getElementById("fileModal"),
|
||||||
|
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"),
|
||||||
|
modalFoldersList: document.getElementById("modalFoldersList"),
|
||||||
|
modalFilesList: document.getElementById("modalFilesList"),
|
||||||
|
modalAddSelectedFilesBtn: document.getElementById("modalAddSelectedFilesBtn"),
|
||||||
};
|
};
|
||||||
|
|
||||||
function initSessionId() {
|
function initSessionId() {
|
||||||
@@ -48,7 +64,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function q(path) {
|
function q(path) {
|
||||||
return path.includes("?") ? `${path}&session_id=${encodeURIComponent(state.sessionId)}` : `${path}?session_id=${encodeURIComponent(state.sessionId)}`;
|
return path.includes("?")
|
||||||
|
? `${path}&session_id=${encodeURIComponent(state.sessionId)}`
|
||||||
|
: `${path}?session_id=${encodeURIComponent(state.sessionId)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function api(path, options = {}) {
|
async function api(path, options = {}) {
|
||||||
@@ -70,7 +88,7 @@
|
|||||||
el.outputBox.textContent = `${label}\n${JSON.stringify(payload, null, 2)}`;
|
el.outputBox.textContent = `${label}\n${JSON.stringify(payload, null, 2)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeActionButton(label, handler, secondary) {
|
function makeBtn(label, handler, secondary) {
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
btn.textContent = label;
|
btn.textContent = label;
|
||||||
if (secondary) btn.className = "secondary";
|
if (secondary) btn.className = "secondary";
|
||||||
@@ -78,27 +96,97 @@
|
|||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLoading(btn, loading) {
|
function setBusy(btn, busy) {
|
||||||
if (!btn) return;
|
if (btn) btn.disabled = busy;
|
||||||
btn.disabled = loading;
|
}
|
||||||
|
|
||||||
|
function updateMeta() {
|
||||||
|
const epCount = state.selectedEpisodes.length;
|
||||||
|
const fileCount = state.selectedFiles.length;
|
||||||
|
const mismatch = epCount !== fileCount;
|
||||||
|
const mismatchText = mismatch ? " mismatch" : " aligned";
|
||||||
|
|
||||||
|
el.episodeMeta.innerHTML = `Rows: <b>${epCount}</b>`;
|
||||||
|
el.fileMeta.innerHTML = `Rows: <b>${fileCount}</b> <span class="${mismatch ? "mismatch" : ""}">${mismatchText}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPair(index) {
|
||||||
|
state.selectedPairIndex = index;
|
||||||
|
renderSelectedEpisodes();
|
||||||
|
renderSelectedFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncScroll(source, target) {
|
||||||
|
if (state.syncScrolling) return;
|
||||||
|
state.syncScrolling = true;
|
||||||
|
target.scrollTop = source.scrollTop;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
state.syncScrolling = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectedEpisodes() {
|
||||||
|
el.selectedEpisodesList.innerHTML = "";
|
||||||
|
state.selectedEpisodes.forEach((item, idx) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
if (state.selectedPairIndex === idx) li.classList.add("selected");
|
||||||
|
|
||||||
|
const left = document.createElement("span");
|
||||||
|
const badge = document.createElement("span");
|
||||||
|
badge.className = "badge";
|
||||||
|
badge.textContent = `#${idx + 1}`;
|
||||||
|
left.appendChild(badge);
|
||||||
|
left.appendChild(document.createTextNode(item.episode.label || item.episode.title || "(episode)"));
|
||||||
|
|
||||||
|
const right = document.createElement("div");
|
||||||
|
right.appendChild(makeBtn("Select", () => selectPair(idx), true));
|
||||||
|
|
||||||
|
li.appendChild(left);
|
||||||
|
li.appendChild(right);
|
||||||
|
li.addEventListener("click", () => selectPair(idx));
|
||||||
|
el.selectedEpisodesList.appendChild(li);
|
||||||
|
});
|
||||||
|
updateMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectedFiles() {
|
||||||
|
el.selectedFilesList.innerHTML = "";
|
||||||
|
state.selectedFiles.forEach((item, idx) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
if (state.selectedPairIndex === idx) li.classList.add("selected");
|
||||||
|
|
||||||
|
const left = document.createElement("span");
|
||||||
|
const badge = document.createElement("span");
|
||||||
|
badge.className = "badge";
|
||||||
|
badge.textContent = `#${idx + 1}`;
|
||||||
|
left.appendChild(badge);
|
||||||
|
left.appendChild(document.createTextNode(item.file.path || item.file.name || "(file)"));
|
||||||
|
|
||||||
|
const right = document.createElement("div");
|
||||||
|
right.appendChild(makeBtn("Select", () => selectPair(idx), true));
|
||||||
|
|
||||||
|
li.appendChild(left);
|
||||||
|
li.appendChild(right);
|
||||||
|
li.addEventListener("click", () => selectPair(idx));
|
||||||
|
el.selectedFilesList.appendChild(li);
|
||||||
|
});
|
||||||
|
updateMeta();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRoots() {
|
async function loadRoots() {
|
||||||
const data = await api("/api/files/roots");
|
const data = await api("/api/files/roots");
|
||||||
state.roots = data.items || [];
|
state.roots = data.items || [];
|
||||||
el.rootsSelect.innerHTML = "";
|
el.modalRootSelect.innerHTML = "";
|
||||||
for (const root of state.roots) {
|
state.roots.forEach((root) => {
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
opt.value = root.id;
|
opt.value = root.id;
|
||||||
opt.textContent = `${root.id}: ${root.path}`;
|
opt.textContent = `${root.id}: ${root.path}`;
|
||||||
el.rootsSelect.appendChild(opt);
|
el.modalRootSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
if (state.roots.length) {
|
||||||
|
el.modalSubpathInput.value = "";
|
||||||
|
await loadModalFolders();
|
||||||
}
|
}
|
||||||
if (state.roots.length > 0) {
|
|
||||||
state.currentSubpath = "";
|
|
||||||
el.subpathInput.value = "";
|
|
||||||
await loadFolders();
|
|
||||||
}
|
|
||||||
out("Roots loaded", data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doSearch() {
|
async function doSearch() {
|
||||||
@@ -109,14 +197,13 @@
|
|||||||
(data.items || []).forEach((item) => {
|
(data.items || []).forEach((item) => {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
const left = document.createElement("span");
|
const left = document.createElement("span");
|
||||||
left.textContent = item.display_name || item.name || "(no name)";
|
left.textContent = item.display_name || item.name || "(series)";
|
||||||
const right = document.createElement("div");
|
const right = document.createElement("div");
|
||||||
const btn = makeActionButton("Select", async () => {
|
right.appendChild(makeBtn("Select", async () => {
|
||||||
state.selectedSeries = item;
|
state.selectedSeries = item;
|
||||||
el.seriesInfo.textContent = `Selected: ${item.display_name || item.name}`;
|
el.seriesInfo.textContent = `Selected: ${item.display_name || item.name}`;
|
||||||
await loadEpisodes();
|
await loadEpisodes();
|
||||||
});
|
}));
|
||||||
right.appendChild(btn);
|
|
||||||
li.appendChild(left);
|
li.appendChild(left);
|
||||||
li.appendChild(right);
|
li.appendChild(right);
|
||||||
el.searchResults.appendChild(li);
|
el.searchResults.appendChild(li);
|
||||||
@@ -131,13 +218,12 @@
|
|||||||
const data = await api(`/api/tvdb/series/${encodeURIComponent(state.selectedSeries.id)}/episodes?order_type=aired`);
|
const data = await api(`/api/tvdb/series/${encodeURIComponent(state.selectedSeries.id)}/episodes?order_type=aired`);
|
||||||
state.episodes = data.items || [];
|
state.episodes = data.items || [];
|
||||||
el.episodesList.innerHTML = "";
|
el.episodesList.innerHTML = "";
|
||||||
|
|
||||||
state.episodes.forEach((episode) => {
|
state.episodes.forEach((episode) => {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
const left = document.createElement("span");
|
const left = document.createElement("span");
|
||||||
left.textContent = episode.label || `${episode.season_number}x${episode.episode_number} ${episode.title}`;
|
left.textContent = episode.label || `${episode.season_number}x${episode.episode_number} ${episode.title}`;
|
||||||
const right = document.createElement("div");
|
const right = document.createElement("div");
|
||||||
const btn = makeActionButton("Add", async () => {
|
right.appendChild(makeBtn("Add", async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
id: episode.id,
|
id: episode.id,
|
||||||
series: state.selectedSeries.name,
|
series: state.selectedSeries.name,
|
||||||
@@ -148,15 +234,13 @@
|
|||||||
aired: episode.aired,
|
aired: episode.aired,
|
||||||
label: episode.label,
|
label: episode.label,
|
||||||
};
|
};
|
||||||
const res = await api(q("/api/session/selected-episodes"), {
|
await api(q("/api/session/selected-episodes"), {
|
||||||
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 loadSelectedEpisodes();
|
await loadSelectedEpisodes();
|
||||||
out("Episode added", res);
|
}));
|
||||||
});
|
|
||||||
right.appendChild(btn);
|
|
||||||
li.appendChild(left);
|
li.appendChild(left);
|
||||||
li.appendChild(right);
|
li.appendChild(right);
|
||||||
el.episodesList.appendChild(li);
|
el.episodesList.appendChild(li);
|
||||||
@@ -166,159 +250,179 @@
|
|||||||
|
|
||||||
async function loadSelectedEpisodes() {
|
async function loadSelectedEpisodes() {
|
||||||
const data = await api(q("/api/session/selected-episodes"));
|
const data = await api(q("/api/session/selected-episodes"));
|
||||||
el.selectedEpisodesList.innerHTML = "";
|
state.selectedEpisodes = data.items || [];
|
||||||
(data.items || []).forEach((item, idx) => {
|
if (state.selectedPairIndex != null && state.selectedPairIndex >= state.selectedEpisodes.length) {
|
||||||
const li = document.createElement("li");
|
state.selectedPairIndex = state.selectedEpisodes.length - 1;
|
||||||
const left = document.createElement("span");
|
}
|
||||||
left.textContent = item.episode.label || item.episode.title || `Episode #${idx + 1}`;
|
renderSelectedEpisodes();
|
||||||
const right = document.createElement("div");
|
|
||||||
right.appendChild(makeActionButton("Up", async () => {
|
|
||||||
if (idx === 0) return;
|
|
||||||
await api(q("/api/session/selected-episodes/reorder"), {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ from_index: idx, to_index: idx - 1 }),
|
|
||||||
});
|
|
||||||
await loadSelectedEpisodes();
|
|
||||||
}, true));
|
|
||||||
right.appendChild(makeActionButton("Down", async () => {
|
|
||||||
if (idx >= data.items.length - 1) return;
|
|
||||||
await api(q("/api/session/selected-episodes/reorder"), {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ from_index: idx, to_index: idx + 1 }),
|
|
||||||
});
|
|
||||||
await loadSelectedEpisodes();
|
|
||||||
}, true));
|
|
||||||
right.appendChild(makeActionButton("Remove", async () => {
|
|
||||||
await api(q(`/api/session/selected-episodes/${item.selection_id}`), { method: "DELETE" });
|
|
||||||
await loadSelectedEpisodes();
|
|
||||||
}, true));
|
|
||||||
li.appendChild(left);
|
|
||||||
li.appendChild(right);
|
|
||||||
el.selectedEpisodesList.appendChild(li);
|
|
||||||
});
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFolders() {
|
async function loadSelectedFiles() {
|
||||||
const rootId = el.rootsSelect.value;
|
const data = await api(q("/api/session/selected-files"));
|
||||||
if (!rootId) throw new Error("No root selected");
|
state.selectedFiles = data.items || [];
|
||||||
const subpath = (el.subpathInput.value || "").trim();
|
if (state.selectedPairIndex != null && state.selectedPairIndex >= state.selectedFiles.length) {
|
||||||
state.currentSubpath = subpath;
|
state.selectedPairIndex = state.selectedFiles.length - 1;
|
||||||
const data = await api(
|
}
|
||||||
`/api/files/folders?root_id=${encodeURIComponent(rootId)}&subpath=${encodeURIComponent(subpath)}&limit=500`
|
renderSelectedFiles();
|
||||||
);
|
return data;
|
||||||
state.folders = data.items || [];
|
}
|
||||||
el.foldersSelect.innerHTML = "";
|
|
||||||
|
|
||||||
|
function openFileModal() {
|
||||||
|
state.modalSelectedFilePaths = new Set();
|
||||||
|
el.fileModalTitle.textContent = "File Discovery";
|
||||||
|
el.modalAddSelectedFilesBtn.style.display = "";
|
||||||
|
el.modalLoadFilesBtn.style.display = "";
|
||||||
|
el.fileModal.classList.remove("hidden");
|
||||||
|
el.fileModal.setAttribute("aria-hidden", "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeFileModal() {
|
||||||
|
el.fileModal.classList.add("hidden");
|
||||||
|
el.fileModal.setAttribute("aria-hidden", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadModalFolders() {
|
||||||
|
const rootId = el.modalRootSelect.value;
|
||||||
|
if (!rootId) throw new Error("No root selected");
|
||||||
|
const subpath = (el.modalSubpathInput.value || "").trim();
|
||||||
|
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");
|
const placeholder = document.createElement("option");
|
||||||
placeholder.value = "";
|
placeholder.value = "";
|
||||||
placeholder.textContent = state.folders.length ? "Choose folder..." : "(No folders)";
|
placeholder.textContent = state.modalFolders.length ? "Choose folder..." : "(No folders)";
|
||||||
el.foldersSelect.appendChild(placeholder);
|
el.modalFolderSelect.appendChild(placeholder);
|
||||||
|
|
||||||
state.folders.forEach((folder) => {
|
el.modalFoldersList.innerHTML = "";
|
||||||
|
state.modalFolders.forEach((folder) => {
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
opt.value = folder.subpath;
|
opt.value = folder.subpath;
|
||||||
opt.textContent = folder.subpath || folder.name;
|
opt.textContent = folder.subpath || folder.name;
|
||||||
el.foldersSelect.appendChild(opt);
|
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);
|
||||||
|
el.modalFoldersList.appendChild(li);
|
||||||
});
|
});
|
||||||
|
|
||||||
out("Folders loaded", data);
|
out("Folders loaded", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function discoverFiles() {
|
async function loadModalFiles() {
|
||||||
const rootId = el.rootsSelect.value;
|
const rootId = el.modalRootSelect.value;
|
||||||
|
const subpath = (el.modalFolderSelect.value || "").trim();
|
||||||
if (!rootId) throw new Error("No root selected");
|
if (!rootId) throw new Error("No root selected");
|
||||||
const selectedFolder = (el.foldersSelect.value || "").trim();
|
if (!subpath) throw new Error("Choose a folder first");
|
||||||
if (!selectedFolder) throw new Error("Choose a folder first");
|
|
||||||
el.subpathInput.value = selectedFolder;
|
|
||||||
state.currentSubpath = selectedFolder;
|
|
||||||
const subpath = encodeURIComponent(selectedFolder);
|
|
||||||
const recursive = el.recursiveInput.checked ? "true" : "false";
|
|
||||||
const data = await api(`/api/files/discover?root_id=${encodeURIComponent(rootId)}&subpath=${subpath}&recursive=${recursive}&limit=200`);
|
|
||||||
state.discoveredFiles = data.items || [];
|
|
||||||
el.discoveredFilesList.innerHTML = "";
|
|
||||||
|
|
||||||
state.discoveredFiles.forEach((file) => {
|
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) => {
|
||||||
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");
|
const right = document.createElement("div");
|
||||||
right.appendChild(makeActionButton("Add", async () => {
|
right.appendChild(makeBtn("Toggle", () => {
|
||||||
const payload = { path: file.path, name: file.name };
|
if (state.modalSelectedFilePaths.has(file.path)) {
|
||||||
const res = await api(q("/api/session/selected-files"), {
|
state.modalSelectedFilePaths.delete(file.path);
|
||||||
method: "POST",
|
li.classList.remove("selected");
|
||||||
headers: { "Content-Type": "application/json" },
|
} else {
|
||||||
body: JSON.stringify({ items: [payload] }),
|
state.modalSelectedFilePaths.add(file.path);
|
||||||
});
|
li.classList.add("selected");
|
||||||
await loadSelectedFiles();
|
}
|
||||||
out("File added", res);
|
|
||||||
}));
|
|
||||||
li.appendChild(left);
|
|
||||||
li.appendChild(right);
|
|
||||||
el.discoveredFilesList.appendChild(li);
|
|
||||||
});
|
|
||||||
out("Discover result", data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadSelectedFiles() {
|
|
||||||
const data = await api(q("/api/session/selected-files"));
|
|
||||||
el.selectedFilesList.innerHTML = "";
|
|
||||||
(data.items || []).forEach((item, idx) => {
|
|
||||||
const li = document.createElement("li");
|
|
||||||
const left = document.createElement("span");
|
|
||||||
left.textContent = item.file.path || item.file.name || `File #${idx + 1}`;
|
|
||||||
const right = document.createElement("div");
|
|
||||||
right.appendChild(makeActionButton("Up", async () => {
|
|
||||||
if (idx === 0) return;
|
|
||||||
await api(q("/api/session/selected-files/reorder"), {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ from_index: idx, to_index: idx - 1 }),
|
|
||||||
});
|
|
||||||
await loadSelectedFiles();
|
|
||||||
}, true));
|
|
||||||
right.appendChild(makeActionButton("Down", async () => {
|
|
||||||
if (idx >= data.items.length - 1) return;
|
|
||||||
await api(q("/api/session/selected-files/reorder"), {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ from_index: idx, to_index: idx + 1 }),
|
|
||||||
});
|
|
||||||
await loadSelectedFiles();
|
|
||||||
}, true));
|
|
||||||
right.appendChild(makeActionButton("Remove", async () => {
|
|
||||||
await api(q(`/api/session/selected-files/${item.selection_id}`), { method: "DELETE" });
|
|
||||||
await loadSelectedFiles();
|
|
||||||
}, true));
|
}, true));
|
||||||
li.appendChild(left);
|
li.appendChild(left);
|
||||||
li.appendChild(right);
|
li.appendChild(right);
|
||||||
el.selectedFilesList.appendChild(li);
|
el.modalFilesList.appendChild(li);
|
||||||
});
|
});
|
||||||
return data;
|
|
||||||
|
out("Discovered files", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function callPreview(path, label) {
|
async function addModalSelectedFiles() {
|
||||||
const data = await api(q(path));
|
const selected = state.modalFiles.filter((f) => state.modalSelectedFilePaths.has(f.path));
|
||||||
out(label, data);
|
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 }),
|
||||||
|
});
|
||||||
|
await loadSelectedFiles();
|
||||||
|
closeFileModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reorderSelectedEpisodes(delta) {
|
||||||
|
if (state.selectedPairIndex == null) throw new Error("Select an episode row first");
|
||||||
|
const from = state.selectedPairIndex;
|
||||||
|
const to = from + delta;
|
||||||
|
if (to < 0 || to >= state.selectedEpisodes.length) return;
|
||||||
|
await api(q("/api/session/selected-episodes/reorder"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ from_index: from, to_index: to }),
|
||||||
|
});
|
||||||
|
state.selectedPairIndex = to;
|
||||||
|
await loadSelectedEpisodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reorderSelectedFiles(delta) {
|
||||||
|
if (state.selectedPairIndex == null) throw new Error("Select a file row first");
|
||||||
|
const from = state.selectedPairIndex;
|
||||||
|
const to = from + delta;
|
||||||
|
if (to < 0 || to >= state.selectedFiles.length) return;
|
||||||
|
await api(q("/api/session/selected-files/reorder"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ from_index: from, to_index: to }),
|
||||||
|
});
|
||||||
|
state.selectedPairIndex = to;
|
||||||
|
await loadSelectedFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeSelectedEpisode() {
|
||||||
|
if (state.selectedPairIndex == null) throw new Error("Select an episode row first");
|
||||||
|
const item = state.selectedEpisodes[state.selectedPairIndex];
|
||||||
|
if (!item) return;
|
||||||
|
await api(q(`/api/session/selected-episodes/${item.selection_id}`), { method: "DELETE" });
|
||||||
|
await loadSelectedEpisodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeSelectedFile() {
|
||||||
|
if (state.selectedPairIndex == null) throw new Error("Select a file row first");
|
||||||
|
const item = state.selectedFiles[state.selectedPairIndex];
|
||||||
|
if (!item) return;
|
||||||
|
await api(q(`/api/session/selected-files/${item.selection_id}`), { method: "DELETE" });
|
||||||
|
await loadSelectedFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeRename() {
|
async function executeRename() {
|
||||||
const data = await api(q("/api/session/rename-execute") + "&confirm=true", {
|
const data = await api(q("/api/session/rename-execute") + "&confirm=true", { method: "POST" });
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
if (data.executed) {
|
if (data.executed) {
|
||||||
out("Rename execute: success", data);
|
out("Rename execute: success", data);
|
||||||
// Keep UI in sync with renamed destinations using existing selected-files endpoints.
|
|
||||||
const renamedFiles = (data.items || [])
|
const renamedFiles = (data.items || [])
|
||||||
.filter((item) => item.status === "renamed")
|
.filter((i) => i.status === "renamed")
|
||||||
.map((item) => ({
|
.map((i) => ({ path: i.destination_path, name: i.proposed_filename }));
|
||||||
path: item.destination_path,
|
|
||||||
name: item.proposed_filename,
|
|
||||||
}));
|
|
||||||
await api(q("/api/session/selected-files"), { method: "DELETE" });
|
await api(q("/api/session/selected-files"), { method: "DELETE" });
|
||||||
if (renamedFiles.length > 0) {
|
if (renamedFiles.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" },
|
||||||
@@ -328,67 +432,79 @@
|
|||||||
await loadSelectedFiles();
|
await loadSelectedFiles();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
out("Rename execute: preflight failed", data);
|
out("Rename execute: preflight failed", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function withHandler(fn, btn) {
|
async function withHandler(fn, btn) {
|
||||||
try {
|
try {
|
||||||
setLoading(btn, true);
|
setBusy(btn, true);
|
||||||
await fn();
|
await fn();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
out("Error", { detail: err.message || String(err) });
|
out("Error", { detail: err.message || String(err) });
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(btn, false);
|
setBusy(btn, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
el.searchBtn.addEventListener("click", () => withHandler(doSearch, el.searchBtn));
|
el.searchBtn.addEventListener("click", () => withHandler(doSearch, el.searchBtn));
|
||||||
el.refreshEpisodesBtn.addEventListener("click", () => withHandler(loadEpisodes, el.refreshEpisodesBtn));
|
el.refreshEpisodesBtn.addEventListener("click", () => withHandler(loadEpisodes, el.refreshEpisodesBtn));
|
||||||
|
|
||||||
el.refreshSelectedEpisodesBtn.addEventListener("click", () => withHandler(loadSelectedEpisodes, el.refreshSelectedEpisodesBtn));
|
el.refreshSelectedEpisodesBtn.addEventListener("click", () => withHandler(loadSelectedEpisodes, el.refreshSelectedEpisodesBtn));
|
||||||
el.clearSelectedEpisodesBtn.addEventListener("click", () =>
|
el.clearSelectedEpisodesBtn.addEventListener("click", () =>
|
||||||
withHandler(async () => {
|
withHandler(async () => {
|
||||||
const res = await api(q("/api/session/selected-episodes"), { method: "DELETE" });
|
await api(q("/api/session/selected-episodes"), { method: "DELETE" });
|
||||||
|
state.selectedPairIndex = null;
|
||||||
await loadSelectedEpisodes();
|
await loadSelectedEpisodes();
|
||||||
out("Selected episodes cleared", res);
|
|
||||||
}, el.clearSelectedEpisodesBtn)
|
}, el.clearSelectedEpisodesBtn)
|
||||||
);
|
);
|
||||||
el.refreshRootsBtn.addEventListener("click", () => withHandler(loadRoots, el.refreshRootsBtn));
|
el.episodeUpBtn.addEventListener("click", () => withHandler(() => reorderSelectedEpisodes(-1), el.episodeUpBtn));
|
||||||
el.rootsSelect.addEventListener("change", () => withHandler(async () => {
|
el.episodeDownBtn.addEventListener("click", () => withHandler(() => reorderSelectedEpisodes(1), el.episodeDownBtn));
|
||||||
state.currentSubpath = "";
|
el.episodeRemoveBtn.addEventListener("click", () => withHandler(removeSelectedEpisode, el.episodeRemoveBtn));
|
||||||
el.subpathInput.value = "";
|
|
||||||
await loadFolders();
|
el.selectFilesBtn.addEventListener("click", () => withHandler(async () => {
|
||||||
el.discoveredFilesList.innerHTML = "";
|
openFileModal();
|
||||||
}, el.rootsSelect));
|
await loadRoots();
|
||||||
el.loadFoldersBtn.addEventListener("click", () => withHandler(loadFolders, el.loadFoldersBtn));
|
}, el.selectFilesBtn));
|
||||||
el.loadFilesBtn.addEventListener("click", () => withHandler(discoverFiles, el.loadFilesBtn));
|
|
||||||
el.foldersSelect.addEventListener("change", () => {
|
|
||||||
const chosen = (el.foldersSelect.value || "").trim();
|
|
||||||
if (chosen) {
|
|
||||||
el.subpathInput.value = chosen;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
el.refreshSelectedFilesBtn.addEventListener("click", () => withHandler(loadSelectedFiles, el.refreshSelectedFilesBtn));
|
el.refreshSelectedFilesBtn.addEventListener("click", () => withHandler(loadSelectedFiles, el.refreshSelectedFilesBtn));
|
||||||
el.clearSelectedFilesBtn.addEventListener("click", () =>
|
el.clearSelectedFilesBtn.addEventListener("click", () =>
|
||||||
withHandler(async () => {
|
withHandler(async () => {
|
||||||
const res = await api(q("/api/session/selected-files"), { method: "DELETE" });
|
await api(q("/api/session/selected-files"), { method: "DELETE" });
|
||||||
|
state.selectedPairIndex = null;
|
||||||
await loadSelectedFiles();
|
await loadSelectedFiles();
|
||||||
out("Selected files cleared", res);
|
|
||||||
}, el.clearSelectedFilesBtn)
|
}, el.clearSelectedFilesBtn)
|
||||||
);
|
);
|
||||||
el.mappingPreviewBtn.addEventListener("click", () => withHandler(() => callPreview("/api/session/mapping-preview", "Mapping preview"), el.mappingPreviewBtn));
|
el.fileUpBtn.addEventListener("click", () => withHandler(() => reorderSelectedFiles(-1), el.fileUpBtn));
|
||||||
el.filenamePreviewBtn.addEventListener("click", () => withHandler(() => callPreview("/api/session/filename-preview", "Filename preview"), el.filenamePreviewBtn));
|
el.fileDownBtn.addEventListener("click", () => withHandler(() => reorderSelectedFiles(1), el.fileDownBtn));
|
||||||
|
el.fileRemoveBtn.addEventListener("click", () => withHandler(removeSelectedFile, el.fileRemoveBtn));
|
||||||
|
|
||||||
|
el.filenamePreviewBtn.addEventListener("click", () => withHandler(async () => out("Filename preview", await api(q("/api/session/filename-preview"))), el.filenamePreviewBtn));
|
||||||
el.renameExecuteBtn.addEventListener("click", () => withHandler(executeRename, el.renameExecuteBtn));
|
el.renameExecuteBtn.addEventListener("click", () => withHandler(executeRename, el.renameExecuteBtn));
|
||||||
|
|
||||||
|
el.closeFileModalBtn.addEventListener("click", closeFileModal);
|
||||||
|
el.fileModal.addEventListener("click", (e) => {
|
||||||
|
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.modalAddSelectedFilesBtn.addEventListener("click", () => withHandler(addModalSelectedFiles, el.modalAddSelectedFilesBtn));
|
||||||
|
|
||||||
|
el.selectedEpisodesList.addEventListener("scroll", () => syncScroll(el.selectedEpisodesList, el.selectedFilesList));
|
||||||
|
el.selectedFilesList.addEventListener("scroll", () => syncScroll(el.selectedFilesList, el.selectedEpisodesList));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
el.sessionMeta.textContent = `session_id: ${state.sessionId}`;
|
el.sessionMeta.textContent = `session_id: ${state.sessionId}`;
|
||||||
el.recursiveInput.checked = true;
|
el.modalRecursiveInput.checked = true;
|
||||||
bindEvents();
|
bindEvents();
|
||||||
await loadRoots();
|
|
||||||
await loadSelectedEpisodes();
|
await loadSelectedEpisodes();
|
||||||
await loadSelectedFiles();
|
await loadSelectedFiles();
|
||||||
|
await loadRoots();
|
||||||
}
|
}
|
||||||
|
|
||||||
init().catch((err) => out("Init error", { detail: err.message || String(err) }));
|
init().catch((err) => out("Init error", { detail: err.message || String(err) }));
|
||||||
|
|||||||
+117
-4
@@ -41,6 +41,9 @@ body {
|
|||||||
border: 1px solid #d7dee9;
|
border: 1px solid #d7dee9;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
min-height: 420px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel h2 {
|
.panel h2 {
|
||||||
@@ -98,6 +101,101 @@ button.secondary {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.linked-list-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 240px;
|
||||||
|
max-height: 440px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linked-list {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
background: #dbeafe;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list li.selected {
|
||||||
|
background: #e0f2fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
border-top: 1px solid #e4eaf2;
|
||||||
|
padding-top: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-row {
|
||||||
|
margin-top: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mismatch {
|
||||||
|
color: #b91c1c;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(2, 6, 23, 0.55);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card {
|
||||||
|
width: min(1100px, 96vw);
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #d7dee9;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-head {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-grid .list {
|
||||||
|
max-height: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#panelSelectedEpisodes .panel-footer button:first-child,
|
||||||
|
#panelSelectedFiles .panel-footer button:last-child,
|
||||||
|
#panelSelectedFiles .advanced-row button:last-child {
|
||||||
|
border-color: #0b3a6e;
|
||||||
|
background: #0b3a6e;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.list li {
|
.list li {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -117,10 +215,6 @@ button.secondary {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.output {
|
|
||||||
margin: 0 12px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#outputBox {
|
#outputBox {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #0b1220;
|
background: #0b1220;
|
||||||
@@ -132,6 +226,21 @@ button.secondary {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.debug-box {
|
||||||
|
margin: 0 12px 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #d7dee9;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-box > summary {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1600px) {
|
@media (max-width: 1600px) {
|
||||||
.grid {
|
.grid {
|
||||||
grid-template-columns: repeat(2, minmax(280px, 1fr));
|
grid-template-columns: repeat(2, minmax(280px, 1fr));
|
||||||
@@ -142,4 +251,8 @@ button.secondary {
|
|||||||
.grid {
|
.grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-35
@@ -33,57 +33,81 @@
|
|||||||
|
|
||||||
<section class="panel" id="panelSelectedEpisodes">
|
<section class="panel" id="panelSelectedEpisodes">
|
||||||
<h2>3. Selected Episodes</h2>
|
<h2>3. Selected Episodes</h2>
|
||||||
<div class="row">
|
<div id="episodeMeta" class="muted"></div>
|
||||||
|
<div class="linked-list-wrap">
|
||||||
|
<ul id="selectedEpisodesList" class="list linked-list"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer">
|
||||||
<button id="refreshSelectedEpisodesBtn">Refresh</button>
|
<button id="refreshSelectedEpisodesBtn">Refresh</button>
|
||||||
|
<button id="episodeUpBtn" class="secondary">Up</button>
|
||||||
|
<button id="episodeDownBtn" class="secondary">Down</button>
|
||||||
|
<button id="episodeRemoveBtn" class="secondary">Remove</button>
|
||||||
<button id="clearSelectedEpisodesBtn">Clear</button>
|
<button id="clearSelectedEpisodesBtn">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
<ul id="selectedEpisodesList" class="list"></ul>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel" id="panelSelectedFiles">
|
<section class="panel" id="panelSelectedFiles">
|
||||||
<h2>4. Selected Files</h2>
|
<h2>4. Selected Files</h2>
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
<div class="row">
|
<div id="fileMeta" class="muted"></div>
|
||||||
<label for="rootsSelect">Root</label>
|
<div class="linked-list-wrap">
|
||||||
<select id="rootsSelect"></select>
|
<ul id="selectedFilesList" class="list linked-list"></ul>
|
||||||
<label for="foldersSelect">Folder</label>
|
|
||||||
<select id="foldersSelect"></select>
|
|
||||||
<button id="refreshRootsBtn">Refresh Roots</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="panel-footer">
|
||||||
<input id="subpathInput" type="text" placeholder="Current subpath (relative)" />
|
<button id="selectFilesBtn">Select Files</button>
|
||||||
<button id="loadFoldersBtn">Load Folders</button>
|
<button id="refreshSelectedFilesBtn" class="secondary">Refresh</button>
|
||||||
<button id="loadFilesBtn">Load Files</button>
|
<button id="fileUpBtn" class="secondary">Up</button>
|
||||||
<label>
|
<button id="fileDownBtn" class="secondary">Down</button>
|
||||||
<input id="recursiveInput" type="checkbox" checked />
|
<button id="fileRemoveBtn" class="secondary">Remove</button>
|
||||||
recursive
|
<button id="clearSelectedFilesBtn">Clear</button>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3>Discovered Files</h3>
|
|
||||||
<ul id="discoveredFilesList" class="list"></ul>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="refreshSelectedFilesBtn">Refresh Selected Files</button>
|
|
||||||
<button id="clearSelectedFilesBtn">Clear Selected Files</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3>Selected Files</h3>
|
|
||||||
<ul id="selectedFilesList" class="list"></ul>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="mappingPreviewBtn">Mapping Preview</button>
|
|
||||||
<button id="filenamePreviewBtn">Filename Preview</button>
|
|
||||||
<button id="renameExecuteBtn">Rename Execute (confirm=true)</button>
|
<button id="renameExecuteBtn">Rename Execute (confirm=true)</button>
|
||||||
|
<button id="filenamePreviewBtn" class="secondary">Filename Preview (Debug)</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<section class="panel output">
|
<details class="debug-box">
|
||||||
<h2>Output</h2>
|
<summary>Debug Output</summary>
|
||||||
<pre id="outputBox"></pre>
|
<pre id="outputBox"></pre>
|
||||||
</section>
|
</details>
|
||||||
|
|
||||||
|
<div id="fileModal" class="modal hidden" aria-hidden="true">
|
||||||
|
<div class="modal-card">
|
||||||
|
<div class="row modal-head">
|
||||||
|
<h3 id="fileModalTitle">File Discovery</h3>
|
||||||
|
<button id="closeFileModalBtn" class="secondary">Close</button>
|
||||||
|
</div>
|
||||||
|
<div class="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>
|
||||||
|
<h4>Folders</h4>
|
||||||
|
<ul id="modalFoldersList" class="list"></ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4>Files</h4>
|
||||||
|
<ul id="modalFilesList" class="list"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="modalAddSelectedFilesBtn">Add Selected Files</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Binary file not shown.
+1
-2
@@ -30,7 +30,7 @@ required = [
|
|||||||
"2. Episodes",
|
"2. Episodes",
|
||||||
"3. Selected Episodes",
|
"3. Selected Episodes",
|
||||||
"4. Selected Files",
|
"4. Selected Files",
|
||||||
"Mapping Preview",
|
"Debug Output",
|
||||||
"Filename Preview",
|
"Filename Preview",
|
||||||
"Rename Execute (confirm=true)",
|
"Rename Execute (confirm=true)",
|
||||||
]
|
]
|
||||||
@@ -77,7 +77,6 @@ required_endpoints = [
|
|||||||
"/api/files/roots",
|
"/api/files/roots",
|
||||||
"/api/files/folders",
|
"/api/files/folders",
|
||||||
"/api/files/discover",
|
"/api/files/discover",
|
||||||
"/api/session/mapping-preview",
|
|
||||||
"/api/session/filename-preview",
|
"/api/session/filename-preview",
|
||||||
"/api/session/rename-execute",
|
"/api/session/rename-execute",
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user