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:
+59
@@ -1113,6 +1113,65 @@ def update_settings(req: UpdateSettingsRequest):
|
||||
save_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}")
|
||||
def get_upload_file(filename: str):
|
||||
file_path = os.path.join(upscaler.UPLOAD_DIR, filename)
|
||||
|
||||
+214
@@ -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() {
|
||||
|
||||
+46
-2
@@ -279,8 +279,13 @@
|
||||
<!-- Custom Temporary Directory -->
|
||||
<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>
|
||||
<input type="text" id="temp-dir-input" placeholder="e.g. /mnt/fast_ssd/temp (Leave empty for default temp/)">
|
||||
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 2px;">
|
||||
<div style="display: flex; gap: 0.5rem; align-items: stretch;">
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -594,6 +599,9 @@
|
||||
<input type="text" id="default-temp-dir-input"
|
||||
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;">
|
||||
<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;">
|
||||
<i class="fa-solid fa-floppy-disk"></i> Save
|
||||
</button>
|
||||
@@ -724,6 +732,42 @@
|
||||
</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">×</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 -->
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user