Add per-folder recursive/top-level-only scanning, fix a dangerous empty-folder-list fallback

- Each source folder now has its own recursive flag (checkbox on add,
  toggle badge on its chip). scan_folder/sync_metadata_from_folder both
  honor it, so "top-level only" actually excludes subfolders end-to-end
  instead of just the tagging queue while sync still scanned recursively.
- New /api/set_folder_recursive to flip an existing folder's mode without
  removing/re-adding it.
- SAFETY FIX: found while testing — when the configured folder list is
  empty, the app fell back to the parent directory of wherever server.py
  lives, which for a typical install (PHOTON dropped directly in the
  user's home folder) means defaulting to the entire home directory.
  Caught this before any scan/start actually ran against it. Fixed so an
  explicitly-configured empty list (config file exists, list is empty)
  is respected as "zero folders" instead of silently substituted with a
  guessed default — the guessed default now only applies on a genuinely
  fresh install where no config file exists yet at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
drjones
2026-07-23 11:22:55 -07:00
parent 1f41ee5352
commit 501bfe2916
2 changed files with 161 additions and 57 deletions

View File

@@ -89,6 +89,14 @@ button:disabled{opacity:.3;cursor:not-allowed}
.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)}
.folderChip .mode{cursor:pointer;color:var(--dim);border:1px solid var(--line);border-radius:8px;
padding:1px 7px;font-size:10px;white-space:nowrap}
.folderChip .mode:hover{color:var(--cyan);border-color:var(--cyan)}
.folderChip .mode.rec{color:var(--cyan-dim)}
.chkRecursiveLabel{display:flex;align-items:center;gap:5px;font-family:var(--font-mono);
font-size:11px;color:var(--dim);white-space:nowrap;padding:0 4px}
.chkRecursiveLabel input{accent-color:var(--cyan)}
.grid{display:grid;grid-template-columns:330px 1fr;gap:14px}
.grid-three{display:grid;grid-template-columns:330px 1fr 320px;gap:14px}
@@ -352,6 +360,9 @@ mark{background:rgba(34,211,238,.28);color:#fff;border-radius:2px;padding:0 1px}
<div class="controls">
<div class="folderRow">
<input id="folder" spellcheck="false" placeholder="/path/to/your/photos — add as many source folders as you like">
<label class="chkRecursiveLabel" title="If off, only the exact folder is scanned — no subfolders">
<input type="checkbox" id="chkRecursive" checked> include subfolders
</label>
<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">
@@ -1021,7 +1032,7 @@ async function api(path, body){
}
/* ---------- multi-folder source list ---------- */
let currentFolders = [];
let currentFolders = []; // [{path, recursive}, ...]
function renderFolderChips(){
const box = $("folderChips");
@@ -1030,8 +1041,11 @@ function renderFolderChips(){
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);
chip.innerHTML = `<span class="path" title="${escapeHtml(f.path)}">${escapeHtml(f.path)}</span>
<span class="mode ${f.recursive?'rec':''}" title="Click to toggle: currently ${f.recursive?'including subfolders':'top-level only'}">${f.recursive?'⊞ subfolders':'▭ top-level only'}</span>
<span class="rm" title="Remove this folder">×</span>`;
chip.querySelector(".rm").onclick = ()=>removeFolder(f.path);
chip.querySelector(".mode").onclick = ()=>toggleFolderRecursive(f);
box.appendChild(chip);
});
}
@@ -1045,8 +1059,13 @@ function applyFolderList(folders){
}
}
async function removeFolder(f){
const j = await api("/api/remove_folder", {folder: f});
async function removeFolder(path){
const j = await api("/api/remove_folder", {folder: path});
if(j.ok) applyFolderList(j.folders);
}
async function toggleFolderRecursive(f){
const j = await api("/api/set_folder_recursive", {folder: f.path, recursive: !f.recursive});
if(j.ok) applyFolderList(j.folders);
}
@@ -1055,7 +1074,7 @@ if($("btnAddFolderTyped")){
const val = $("folder").value.trim();
if(!val) return;
$("btnAddFolderTyped").disabled = true;
const j = await api("/api/add_folder", {folder: val});
const j = await api("/api/add_folder", {folder: val, recursive: $("chkRecursive").checked});
$("btnAddFolderTyped").disabled = false;
if(j.ok){
$("folder").value = "";
@@ -1069,7 +1088,7 @@ if($("btnBrowseFolder")){
$("btnBrowseFolder").onclick = async ()=>{
$("btnBrowseFolder").disabled = true;
showToast("Opening folder chooser…", "info");
const j = await api("/api/choose_folder", {});
const j = await api("/api/choose_folder", {recursive: $("chkRecursive").checked});
$("btnBrowseFolder").disabled = false;
if(j.ok && j.folder){
showToast("Added folder: " + j.folder, "ok");