diff --git a/index.html b/index.html
index 38763d8..cdfca83 100644
--- a/index.html
+++ b/index.html
@@ -341,6 +341,7 @@ mark{background:rgba(34,211,238,.28);color:#fff;border-radius:2px;padding:0 1px}
+
@@ -991,6 +992,20 @@ async function api(path, body){
return j;
}
+if($("btnBrowseFolder")){
+ $("btnBrowseFolder").onclick = async ()=>{
+ $("btnBrowseFolder").disabled = true;
+ 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");
+ const scanRes = await api("/api/scan",{folder: j.folder});
+ if(scanRes.images!=null){ scanned=true; setBadge("idle"); }
+ }
+ };
+}
+
$("btnScan").onclick = async ()=>{
const j = await api("/api/scan",{folder:$("folder").value});
if(j.images!=null){ scanned=true; setBadge("idle"); }
diff --git a/launch.command b/launch.command
new file mode 100755
index 0000000..790b975
--- /dev/null
+++ b/launch.command
@@ -0,0 +1,20 @@
+#!/bin/bash
+# PHOTON — Double-click macOS launcher
+# Automatically starts the server and opens the console in your default web browser.
+
+CD_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd "$CD_DIR"
+
+echo "======================================================="
+echo " PHOTON — Local Photo Intelligence Console Launch "
+echo "======================================================="
+echo ""
+echo "Starting local HTTP server..."
+python3 server.py &
+SERVER_PID=$!
+
+sleep 1.5
+echo "Opening browser at http://localhost:8765 ..."
+open "http://localhost:8765"
+
+wait $SERVER_PID
diff --git a/server.py b/server.py
index d06cc07..7d53039 100644
--- a/server.py
+++ b/server.py
@@ -214,6 +214,19 @@ def write_failures():
except Exception:
pass
+def resolve_portable_path(path):
+ if not path:
+ return path
+ if os.path.exists(path):
+ return path
+ with S.lock:
+ curr_folder = S.folder
+ if curr_folder and os.path.isdir(curr_folder):
+ cand = os.path.join(curr_folder, os.path.basename(path))
+ if os.path.exists(cand):
+ return cand
+ return path
+
def load_journal():
n = 0
with S.lock:
@@ -227,8 +240,9 @@ def load_journal():
continue
try:
rec = json.loads(line)
+ path = resolve_portable_path(rec.get("path", ""))
with S.lock:
- S.done_paths.add(rec["path"])
+ S.done_paths.add(path)
cat = rec.get("category")
if cat not in S.cat_counts:
S.cat_counts[cat] = 0
@@ -246,7 +260,6 @@ def trash_or_remove_file(path):
if not os.path.exists(path):
return True
try:
- # Try macOS Trash via AppleScript
cmd = ["osascript", "-e", f'tell application "Finder" to delete POSIX file "{os.path.abspath(path)}"']
r = subprocess.run(cmd, capture_output=True, timeout=10)
if r.returncode == 0:
@@ -262,7 +275,6 @@ def trash_or_remove_file(path):
def remove_photo_records(path):
abs_path = os.path.abspath(path)
- # 1. Remove symlink in _organized
with S.lock:
folder = S.folder
org_root = os.path.join(folder, "_organized")
@@ -278,7 +290,6 @@ def remove_photo_records(path):
except Exception:
pass
- # 2. Clean thumbnail and view caches
try:
h = hashlib.md5(abs_path.encode()).hexdigest()
t1 = os.path.join(tempfile.gettempdir(), "photon_thumbs", f"{h}.jpg")
@@ -290,7 +301,6 @@ def remove_photo_records(path):
except Exception:
pass
- # 3. Rewrite journal without this path
if os.path.exists(JOURNAL):
lines_to_keep = []
with open(JOURNAL, "r", encoding="utf-8") as f:
@@ -308,7 +318,6 @@ def remove_photo_records(path):
for l in lines_to_keep:
f.write(l + "\n")
- # 4. Remove from State
with S.lock:
if abs_path in S.done_paths:
S.done_paths.remove(abs_path)
@@ -356,7 +365,8 @@ def load_search_index():
continue
try:
rec = json.loads(line)
- path = rec.get("path", "")
+ path = resolve_portable_path(rec.get("path", ""))
+ rec["path"] = path
name = os.path.basename(path)
rec["name"] = name
rec["_ext"] = os.path.splitext(name)[1].lower()
@@ -1678,6 +1688,22 @@ class Handler(BaseHTTPRequestHandler):
self._json({"images": len(images), "videos": len(videos),
"sidecars": sidecars, "alreadyDone": done})
+ elif self.path == "/api/choose_folder":
+ try:
+ cmd = ["osascript", "-e", 'POSIX path of (choose folder with prompt "Select Photo Folder to Organize:")']
+ r = subprocess.run(cmd, capture_output=True, timeout=60)
+ if r.returncode == 0:
+ chosen = r.stdout.decode("utf-8").strip()
+ if chosen and os.path.isdir(chosen):
+ with S.lock:
+ S.folder = chosen
+ save_folder_config(chosen)
+ self._json({"ok": True, "folder": chosen})
+ return
+ self._json({"error": "No folder selected"}, 400)
+ except Exception as e:
+ self._json({"error": str(e)}, 500)
+
elif self.path == "/api/start":
with S.lock:
if S.status == "running":