Implement full system directory browser modal for temp directories

- Add GET /api/system/browse-dir and POST /api/system/create-dir endpoints
- Add Folder Browser Modal overlay in index.html
- Add browse buttons to both job-specific and default setting temp dir inputs
- Add JS helpers and event handlers for directory list loading, parent navigation, directory selection, and new folder creation
This commit is contained in:
2026-06-29 10:02:58 -04:00
parent 006eece417
commit c7b637e70a
3 changed files with 319 additions and 2 deletions
+59
View File
@@ -1113,6 +1113,65 @@ def update_settings(req: UpdateSettingsRequest):
save_settings(settings) save_settings(settings)
return {"status": "saved", "settings": settings} return {"status": "saved", "settings": settings}
class CreateDirRequest(BaseModel):
parent_path: str
name: str
@app.get("/api/system/browse-dir")
def browse_dir(path: str = ""):
"""Browse directories on the system"""
if not path:
path = os.path.expanduser("~")
path = os.path.abspath(path)
if not os.path.exists(path):
raise HTTPException(status_code=404, detail="Path does not exist")
if not os.path.isdir(path):
raise HTTPException(status_code=400, detail="Path is not a directory")
try:
entries = []
# Add parent directory entry if not at root
parent = os.path.dirname(path)
if parent != path:
entries.append({
"name": "..",
"path": parent,
"is_dir": True
})
for name in sorted(os.listdir(path)):
full_path = os.path.join(path, name)
try:
if os.path.isdir(full_path):
entries.append({
"name": name,
"path": full_path,
"is_dir": True
})
except PermissionError:
continue
return {
"current_path": path,
"entries": entries
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/system/create-dir")
def create_dir(req: CreateDirRequest):
"""Create a new directory"""
full_path = os.path.abspath(os.path.join(req.parent_path, req.name))
try:
os.makedirs(full_path, exist_ok=True)
return {"status": "ok", "path": full_path}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/api/uploads/{filename}") @app.get("/api/uploads/{filename}")
def get_upload_file(filename: str): def get_upload_file(filename: str):
file_path = os.path.join(upscaler.UPLOAD_DIR, filename) file_path = os.path.join(upscaler.UPLOAD_DIR, filename)
+214
View File
@@ -2171,6 +2171,220 @@ async function initializeApp() {
} }
}); });
} }
// Wire up folder browser modals
const browseJobTempBtn = document.getElementById("browse-job-temp-btn");
const browseDefaultTempBtn = document.getElementById("browse-default-temp-btn");
const folderModalCloseBtn = document.getElementById("folder-modal-close-btn");
const folderCancelBtn = document.getElementById("folder-cancel-btn");
const folderSelectBtn = document.getElementById("folder-select-btn");
const folderUpBtn = document.getElementById("folder-up-btn");
const createFolderBtn = document.getElementById("create-folder-btn");
const newFolderNameInput = document.getElementById("new-folder-name");
if (browseJobTempBtn) {
browseJobTempBtn.addEventListener("click", () => {
openFolderBrowser(document.getElementById("temp-dir-input"));
});
}
if (browseDefaultTempBtn) {
browseDefaultTempBtn.addEventListener("click", () => {
openFolderBrowser(document.getElementById("default-temp-dir-input"));
});
}
if (folderModalCloseBtn) {
folderModalCloseBtn.addEventListener("click", closeFolderBrowser);
}
if (folderCancelBtn) {
folderCancelBtn.addEventListener("click", closeFolderBrowser);
}
// Close modal when clicking outside card
const folderModal = document.getElementById("folder-modal");
if (folderModal) {
folderModal.addEventListener("click", (e) => {
if (e.target === folderModal) {
closeFolderBrowser();
}
});
}
if (folderSelectBtn) {
folderSelectBtn.addEventListener("click", () => {
if (activeTargetInput && currentBrowsePath) {
activeTargetInput.value = currentBrowsePath;
activeTargetInput.dispatchEvent(new Event("change"));
activeTargetInput.dispatchEvent(new Event("input"));
}
closeFolderBrowser();
});
}
if (folderUpBtn) {
folderUpBtn.addEventListener("click", () => {
if (currentBrowsePath) {
const parts = currentBrowsePath.split("/");
if (parts.length > 1) {
parts.pop();
const parent = parts.join("/") || "/";
loadFolderDir(parent);
}
}
});
}
if (createFolderBtn) {
createFolderBtn.addEventListener("click", createFolder);
}
if (newFolderNameInput) {
newFolderNameInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
createFolder();
}
});
}
}
/* ----------------------------------------------------
Folder Browser Modal & API Helpers
---------------------------------------------------- */
let activeTargetInput = null;
let currentBrowsePath = "";
function openFolderBrowser(targetInput) {
activeTargetInput = targetInput;
const modal = document.getElementById("folder-modal");
if (!modal) return;
const statusDiv = document.getElementById("folder-modal-status");
if (statusDiv) statusDiv.textContent = "";
let initialPath = targetInput ? targetInput.value.trim() : "";
modal.style.display = "flex";
document.body.style.overflow = "hidden";
loadFolderDir(initialPath);
}
function closeFolderBrowser() {
const modal = document.getElementById("folder-modal");
if (modal) {
modal.style.display = "none";
document.body.style.overflow = "";
}
activeTargetInput = null;
}
async function loadFolderDir(path) {
const listContainer = document.getElementById("folder-list");
const currentPathInput = document.getElementById("folder-current-path");
const statusDiv = document.getElementById("folder-modal-status");
if (!listContainer) return;
listContainer.innerHTML = '<div style="text-align: center; padding: 2rem; color: var(--text-muted);"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</div>';
try {
const url = `/api/system/browse-dir?path=${encodeURIComponent(path)}`;
const res = await fetch(url);
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || "Failed to load directory.");
}
const data = await res.json();
currentBrowsePath = data.current_path;
if (currentPathInput) currentPathInput.value = currentBrowsePath;
listContainer.innerHTML = "";
const entries = data.entries || [];
if (entries.length === 0) {
listContainer.innerHTML = '<div style="text-align: center; padding: 2rem; color: var(--text-muted);">Empty directory</div>';
return;
}
entries.forEach(entry => {
const row = document.createElement("div");
row.style.display = "flex";
row.style.alignItems = "center";
row.style.gap = "0.75rem";
row.style.padding = "0.6rem 0.85rem";
row.style.borderRadius = "6px";
row.style.cursor = "pointer";
row.style.transition = "background 0.2s";
row.addEventListener("mouseenter", () => {
row.style.background = "rgba(255, 255, 255, 0.05)";
});
row.addEventListener("mouseleave", () => {
row.style.background = "";
});
const icon = document.createElement("i");
if (entry.name === "..") {
icon.className = "fa-solid fa-arrow-turn-up";
icon.style.color = "var(--text-muted)";
icon.style.transform = "rotate(-90deg)";
} else {
icon.className = "fa-solid fa-folder";
icon.style.color = "var(--primary)";
}
row.appendChild(icon);
const label = document.createElement("span");
label.textContent = entry.name;
label.style.fontSize = "0.9rem";
label.style.fontFamily = "monospace";
row.appendChild(label);
row.addEventListener("click", () => {
loadFolderDir(entry.path);
});
listContainer.appendChild(row);
});
} catch (err) {
listContainer.innerHTML = `<div style="text-align: center; padding: 2rem; color: var(--danger);"><i class="fa-solid fa-triangle-exclamation"></i> Error: ${err.message}</div>`;
if (statusDiv) statusDiv.textContent = err.message;
}
}
async function createFolder() {
const nameInput = document.getElementById("new-folder-name");
const statusDiv = document.getElementById("folder-modal-status");
if (!nameInput || !nameInput.value.trim() || !currentBrowsePath) return;
const folderName = nameInput.value.trim();
if (statusDiv) statusDiv.textContent = "";
try {
const res = await fetch("/api/system/create-dir", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
parent_path: currentBrowsePath,
name: folderName
})
});
if (res.ok) {
nameInput.value = "";
const data = await res.json();
loadFolderDir(data.path);
} else {
const err = await res.json().catch(() => ({}));
if (statusDiv) statusDiv.textContent = err.detail || "Failed to create folder.";
}
} catch (err) {
console.error(err);
if (statusDiv) statusDiv.textContent = "Network error creating folder.";
}
} }
async function loadTempInfo() { async function loadTempInfo() {
+46 -2
View File
@@ -279,8 +279,13 @@
<!-- Custom Temporary Directory --> <!-- Custom Temporary Directory -->
<div class="form-group"> <div class="form-group">
<label for="temp-dir-input"><i class="fa-solid fa-folder-open" style="color: var(--primary); margin-right: 0.25rem;"></i> Custom Temporary Files Directory</label> <label for="temp-dir-input"><i class="fa-solid fa-folder-open" style="color: var(--primary); margin-right: 0.25rem;"></i> Custom Temporary Files Directory</label>
<input type="text" id="temp-dir-input" placeholder="e.g. /mnt/fast_ssd/temp (Leave empty for default temp/)"> <div style="display: flex; gap: 0.5rem; align-items: stretch;">
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 2px;"> <input type="text" id="temp-dir-input" placeholder="e.g. /mnt/fast_ssd/temp (Leave empty for default temp/)" style="flex: 1;">
<button type="button" id="browse-job-temp-btn" class="btn btn-secondary" style="padding: 0 1rem;" title="Browse files">
<i class="fa-solid fa-folder-open"></i>
</button>
</div>
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 4px;">
Specify a custom location for temporary image frames to prevent running out of primary drive space. Specify a custom location for temporary image frames to prevent running out of primary drive space.
</p> </p>
</div> </div>
@@ -594,6 +599,9 @@
<input type="text" id="default-temp-dir-input" <input type="text" id="default-temp-dir-input"
placeholder="e.g. /mnt/fast_ssd/upscaler_temp (Leave empty for project default temp/)" placeholder="e.g. /mnt/fast_ssd/upscaler_temp (Leave empty for project default temp/)"
style="flex: 1; padding: 0.6rem 0.85rem; border-radius: 8px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-primary); font-size: 0.88rem; font-family: 'JetBrains Mono', monospace;"> style="flex: 1; padding: 0.6rem 0.85rem; border-radius: 8px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-primary); font-size: 0.88rem; font-family: 'JetBrains Mono', monospace;">
<button type="button" id="browse-default-temp-btn" class="btn btn-secondary" style="padding: 0 1rem;" title="Browse files">
<i class="fa-solid fa-folder-open"></i>
</button>
<button type="button" id="save-temp-dir-btn" class="btn btn-primary btn-small" style="white-space: nowrap; padding: 0.6rem 1.2rem;"> <button type="button" id="save-temp-dir-btn" class="btn btn-primary btn-small" style="white-space: nowrap; padding: 0.6rem 1.2rem;">
<i class="fa-solid fa-floppy-disk"></i> Save <i class="fa-solid fa-floppy-disk"></i> Save
</button> </button>
@@ -724,6 +732,42 @@
</div> </div>
</div> </div>
<!-- Folder Browser Modal -->
<div id="folder-modal" class="modal-overlay" style="display: none; z-index: 1100;">
<div class="modal-card" style="max-width: 600px; display: flex; flex-direction: column; max-height: 80vh;">
<div class="modal-header">
<h3><i class="fa-solid fa-folder-open"></i> Select Directory</h3>
<button type="button" class="close-btn" id="folder-modal-close-btn">&times;</button>
</div>
<div class="modal-body" style="overflow-y: hidden; display: flex; flex-direction: column; height: 60vh; padding: 1.5rem; gap: 1rem;">
<div style="display: flex; gap: 0.5rem; align-items: center;">
<button type="button" id="folder-up-btn" class="btn btn-secondary btn-small" style="padding: 0.5rem 0.75rem;">
<i class="fa-solid fa-arrow-up"></i> Up
</button>
<input type="text" id="folder-current-path" style="flex: 1; padding: 0.5rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-primary); font-family: monospace; font-size: 0.85rem;" readonly>
</div>
<div id="folder-list" style="flex: 1; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 8px; background: rgba(0, 0, 0, 0.2); padding: 0.5rem;">
<!-- Folder items list -->
</div>
<div style="display: flex; gap: 0.5rem; align-items: center; border-top: 1px solid var(--border-color); padding-top: 1rem;">
<input type="text" id="new-folder-name" placeholder="New folder name" style="flex: 1; padding: 0.5rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-primary); font-size: 0.88rem;">
<button type="button" id="create-folder-btn" class="btn btn-secondary btn-small" style="padding: 0.5rem 1rem;">
<i class="fa-solid fa-folder-plus"></i> Create Folder
</button>
</div>
<div id="folder-modal-status" style="font-size: 0.78rem; color: var(--danger); min-height: 1.2em;"></div>
<div style="display: flex; justify-content: flex-end; gap: 0.75rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
<button type="button" id="folder-cancel-btn" class="btn btn-secondary">Cancel</button>
<button type="button" id="folder-select-btn" class="btn btn-primary">Select Current Directory</button>
</div>
</div>
</div>
</div>
<!-- Javascript Orchestration --> <!-- Javascript Orchestration -->
<script src="app.js"></script> <script src="app.js"></script>
</body> </body>