Add support for multiple source folders
- Server: S.folder (singular) -> S.folders (list). New /api/add_folder and /api/remove_folder endpoints; /api/scan and /api/choose_folder updated to work with the list (choose_folder now appends instead of replacing). Legacy single-folder config migrates transparently. - All folders feed one combined pending queue/journal/search, per the simpler of the two designs discussed. - organize_alias now resolves each photo's own source folder via find_owning_folder(), so category aliases land next to the original instead of in one designated folder -- verified live with two configured folders that the alias for each photo lands in its own folder and nothing leaks into the other. - Delete path-safety, thumbnail/photo serving, resolve_portable_path, undo_all, and sync-on-scan all extended to check/act across every configured folder instead of just one. - Frontend: folder list renders as removable chips; Add (typed path) and Browse (native picker, now appends) both wired to the new endpoints; Rescan All re-scans the current set without changing it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
78
index.html
78
index.html
@@ -80,6 +80,16 @@ button:disabled{opacity:.3;cursor:not-allowed}
|
||||
font-family:var(--font-mono);font-size:12px;padding:9px 12px;border-radius:4px}
|
||||
.folderRow input:focus{outline:none;border-color:var(--cyan)}
|
||||
|
||||
.folderChips{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px}
|
||||
.folderChips:empty{display:none}
|
||||
.folderChip{display:flex;align-items:center;gap:8px;background:var(--panel);
|
||||
border:1px solid var(--line);border-radius:14px;padding:5px 6px 5px 12px;
|
||||
font-family:var(--font-mono);font-size:11px;color:var(--txt)}
|
||||
.folderChip .path{max-width:340px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.folderChip .rm{cursor:pointer;color:var(--dim);border:1px solid var(--line);border-radius:50%;
|
||||
width:16px;height:16px;display:flex;align-items:center;justify-content:center;font-size:11px;line-height:1}
|
||||
.folderChip .rm:hover{color:var(--red);border-color:var(--red)}
|
||||
|
||||
.grid{display:grid;grid-template-columns:330px 1fr;gap:14px}
|
||||
.grid-three{display:grid;grid-template-columns:330px 1fr 320px;gap:14px}
|
||||
@media(max-width:1200px){.grid,.grid-three{grid-template-columns:1fr}}
|
||||
@@ -338,12 +348,14 @@ mark{background:rgba(34,211,238,.28);color:#fff;border-radius:2px;padding:0 1px}
|
||||
|
||||
<!-- ==================== CONSOLE TAB ==================== -->
|
||||
<div id="tab-console" class="tab-content active">
|
||||
<div id="folderChips" class="folderChips"></div>
|
||||
<div class="controls">
|
||||
<div class="folderRow">
|
||||
<input id="folder" spellcheck="false" placeholder="/path/to/your/photos">
|
||||
<button id="btnBrowseFolder" title="Open native macOS folder chooser">📁 Browse</button>
|
||||
<input id="folder" spellcheck="false" placeholder="/path/to/your/photos — add as many source folders as you like">
|
||||
<button id="btnAddFolderTyped" title="Add this typed path as a source folder">+ Add</button>
|
||||
<button id="btnBrowseFolder" title="Open native macOS folder chooser to add a source folder">📁 Browse</button>
|
||||
<input type="file" id="fallbackFolderInput" webkitdirectory style="display:none">
|
||||
<button id="btnScan">◈ Scan Directory</button>
|
||||
<button id="btnScan" title="Re-scan every configured source folder now">⟳ Rescan All</button>
|
||||
</div>
|
||||
<button id="btnStart" class="btn-primary" disabled>▶ Start Pipeline</button>
|
||||
<button id="btnPause" disabled>❚❚ Pause</button>
|
||||
@@ -1008,6 +1020,51 @@ async function api(path, body){
|
||||
return j;
|
||||
}
|
||||
|
||||
/* ---------- multi-folder source list ---------- */
|
||||
let currentFolders = [];
|
||||
|
||||
function renderFolderChips(){
|
||||
const box = $("folderChips");
|
||||
if(!box) return;
|
||||
box.innerHTML = "";
|
||||
currentFolders.forEach(f=>{
|
||||
const chip = document.createElement("div");
|
||||
chip.className = "folderChip";
|
||||
chip.innerHTML = `<span class="path" title="${escapeHtml(f)}">${escapeHtml(f)}</span><span class="rm" title="Remove this folder">×</span>`;
|
||||
chip.querySelector(".rm").onclick = ()=>removeFolder(f);
|
||||
box.appendChild(chip);
|
||||
});
|
||||
}
|
||||
|
||||
function applyFolderList(folders){
|
||||
if(folders && folders.length){
|
||||
currentFolders = folders;
|
||||
renderFolderChips();
|
||||
scanned = true;
|
||||
setBadge("idle");
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFolder(f){
|
||||
const j = await api("/api/remove_folder", {folder: f});
|
||||
if(j.ok) applyFolderList(j.folders);
|
||||
}
|
||||
|
||||
if($("btnAddFolderTyped")){
|
||||
$("btnAddFolderTyped").onclick = async ()=>{
|
||||
const val = $("folder").value.trim();
|
||||
if(!val) return;
|
||||
$("btnAddFolderTyped").disabled = true;
|
||||
const j = await api("/api/add_folder", {folder: val});
|
||||
$("btnAddFolderTyped").disabled = false;
|
||||
if(j.ok){
|
||||
$("folder").value = "";
|
||||
showToast("Added folder: " + val, "ok");
|
||||
applyFolderList(j.folders);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if($("btnBrowseFolder")){
|
||||
$("btnBrowseFolder").onclick = async ()=>{
|
||||
$("btnBrowseFolder").disabled = true;
|
||||
@@ -1015,10 +1072,8 @@ if($("btnBrowseFolder")){
|
||||
const j = await api("/api/choose_folder", {});
|
||||
$("btnBrowseFolder").disabled = false;
|
||||
if(j.ok && j.folder){
|
||||
$("folder").value = j.folder;
|
||||
showToast("Selected folder: " + j.folder, "ok");
|
||||
scanned = true;
|
||||
setBadge("idle");
|
||||
showToast("Added folder: " + j.folder, "ok");
|
||||
applyFolderList(j.folders);
|
||||
} else if($("fallbackFolderInput")){
|
||||
$("fallbackFolderInput").click();
|
||||
}
|
||||
@@ -1026,8 +1081,8 @@ if($("btnBrowseFolder")){
|
||||
}
|
||||
|
||||
$("btnScan").onclick = async ()=>{
|
||||
const j = await api("/api/scan",{folder:$("folder").value});
|
||||
if(j.images!=null){ scanned=true; setBadge("idle"); }
|
||||
const j = await api("/api/scan", currentFolders.length ? {folders: currentFolders} : {});
|
||||
if(j.images!=null){ applyFolderList(j.folders); }
|
||||
};
|
||||
|
||||
$("btnStart").onclick = ()=>{
|
||||
@@ -1061,7 +1116,7 @@ $("btnRetryFailed").onclick = async ()=>{
|
||||
};
|
||||
|
||||
$("btnUndoAll").onclick = async ()=>{
|
||||
if(confirm("☣ WARNING ☣\nThis will remove all PHOTON embedded descriptions/categories/keywords from ALL files in the scanned folder. It will also clear organized symlinks, reset the database and delete the failures log.\n\nTHIS ACTION CANNOT BE UNDONE.\n\nDo you want to proceed?")){
|
||||
if(confirm("☣ WARNING ☣\nThis will remove all PHOTON embedded descriptions/categories/keywords from ALL files in every configured source folder ("+(currentFolders.length||"all")+"). It will also clear organized symlinks, reset the database and delete the failures log.\n\nTHIS ACTION CANNOT BE UNDONE.\n\nDo you want to proceed?")){
|
||||
$("btnUndoAll").disabled = true;
|
||||
const j = await api("/api/undo_all", {});
|
||||
$("btnUndoAll").disabled = false;
|
||||
@@ -2052,7 +2107,8 @@ function connect(){
|
||||
CATS = st.categories||[];
|
||||
renderCats(st.catCounts||{});
|
||||
(st.log||[]).forEach(addLog);
|
||||
if(st.folder) $("folder").value = st.folder;
|
||||
currentFolders = st.folders || [];
|
||||
renderFolderChips();
|
||||
applyStats(st);
|
||||
setBadge(st.status||"idle");
|
||||
if(st.total>0) scanned=true, setBadge(st.status||"idle");
|
||||
|
||||
Reference in New Issue
Block a user