Compare commits
3 Commits
e4e4de319d
...
32775ef83f
| Author | SHA1 | Date | |
|---|---|---|---|
| 32775ef83f | |||
| 39d19cf2f8 | |||
| c9c1a50103 |
@@ -105,7 +105,7 @@ remote-install:
|
||||
# ── Remote: ongoing maintenance ────────────────────────────────────────────────
|
||||
|
||||
remote-fetch:
|
||||
$(SSH) "git -C $(SITE_CONFIG_DIR) pull"
|
||||
$(SSH) "git -C $(SITE_CONFIG_DIR) checkout main && git -C $(SITE_CONFIG_DIR) pull"
|
||||
|
||||
remote-install-plugins:
|
||||
$(SSH) "cd $(WEBROOT) && php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y"
|
||||
|
||||
@@ -1,9 +1,77 @@
|
||||
from flask import Blueprint, current_app, render_template, request
|
||||
from app.state import load_state
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Blueprint, current_app, redirect, render_template, request
|
||||
from app.immich import ImmichClient
|
||||
from app.state import TripState, Photo, load_state, save_state
|
||||
|
||||
bp = Blueprint("albums", __name__)
|
||||
|
||||
|
||||
def _client():
|
||||
return ImmichClient(current_app.config["IMMICH_URL"],
|
||||
current_app.config["IMMICH_API_KEY"])
|
||||
|
||||
|
||||
@bp.get("/")
|
||||
def index():
|
||||
try:
|
||||
albums = _client().list_albums()
|
||||
error = None
|
||||
except ConnectionError as e:
|
||||
albums = []
|
||||
error = str(e)
|
||||
state_dir = Path(current_app.config["STATE_DIR"])
|
||||
for album in albums:
|
||||
album["has_state"] = (state_dir / f"{album['id']}.json").exists()
|
||||
return render_template("phase1.html", albums=albums, error=error,
|
||||
current_phase="", album_id=None,
|
||||
phase_stale=[], notes_content="")
|
||||
|
||||
|
||||
@bp.post("/select")
|
||||
def select():
|
||||
album_ids = request.form.getlist("album_ids[]")
|
||||
grav_trip_slug = request.form["grav_trip_slug"].strip()
|
||||
start_over = request.form.get("start_over") == "1"
|
||||
|
||||
if len(album_ids) == 1:
|
||||
primary_id = album_ids[0]
|
||||
else:
|
||||
primary_id = "__merged__" + "_".join(sorted(album_ids))
|
||||
|
||||
existing = load_state(primary_id, current_app)
|
||||
if existing and not start_over:
|
||||
return redirect(f"/{existing.phase}?album_id={primary_id}")
|
||||
|
||||
# Fetch and merge assets, deduplicating by asset ID
|
||||
all_assets = {}
|
||||
album_name_parts = []
|
||||
for aid in album_ids:
|
||||
album = _client().get_album(aid)
|
||||
album_name_parts.append(album["albumName"])
|
||||
for asset in album["assets"]:
|
||||
if asset["id"] not in all_assets:
|
||||
all_assets[asset["id"]] = asset
|
||||
|
||||
photos = [
|
||||
Photo(id=a["id"], original_filename=a["originalFileName"],
|
||||
local_datetime=a["localDateTime"])
|
||||
for a in sorted(all_assets.values(), key=lambda x: x["localDateTime"])
|
||||
]
|
||||
for i, p in enumerate(photos):
|
||||
p.order = i
|
||||
|
||||
state = TripState(
|
||||
album_id=primary_id,
|
||||
album_name=", ".join(album_name_parts),
|
||||
grav_trip_slug=grav_trip_slug,
|
||||
photos=photos,
|
||||
)
|
||||
save_state(state, current_app)
|
||||
return redirect(f"/triage?album_id={primary_id}")
|
||||
|
||||
|
||||
# TODO(task-6): replace this stub with the real triage route
|
||||
@bp.get("/triage")
|
||||
def triage():
|
||||
album_id = request.args.get("album_id", "")
|
||||
|
||||
@@ -3,8 +3,6 @@ from app.state import load_state, save_state
|
||||
|
||||
bp = Blueprint("notes", __name__)
|
||||
|
||||
PHASE_ORDER = ["triage", "curate", "group", "write", "export"]
|
||||
|
||||
|
||||
@bp.post("/notes/save")
|
||||
def save_notes():
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body class="min-h-screen bg-base-200">
|
||||
<body class="min-h-screen bg-base-200" x-data="notesApp({{ notes_content | tojson }}, '{{ album_id }}')">
|
||||
|
||||
<!-- Navbar -->
|
||||
<div class="navbar bg-base-100 shadow-sm sticky top-0 z-40">
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<div class="navbar-end px-4">
|
||||
{% if album_id %}
|
||||
<button class="btn btn-ghost btn-sm" @click="open = !open" x-data>📝 Notes</button>
|
||||
<button class="btn btn-ghost btn-sm" @click="open = !open">📝 Notes</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,7 +45,7 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Body with notes drawer -->
|
||||
<div class="flex relative" x-data="notesApp('{{ notes_content | e }}', '{{ album_id }}')">
|
||||
<div class="flex relative">
|
||||
<div class="flex-1 min-w-0 transition-all" :class="open ? 'mr-80' : ''">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="p-6 max-w-5xl mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-4">Select Album</h1>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-error mb-4">
|
||||
<span>Cannot reach Immich: {{ error }}</span>
|
||||
<a href="/" class="btn btn-sm">Retry</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/select">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||
{% for album in albums %}
|
||||
<label class="album-card card bg-base-100 shadow cursor-pointer hover:shadow-lg transition"
|
||||
data-album-id="{{ album.id }}">
|
||||
<figure class="h-40 overflow-hidden">
|
||||
<img src="/proxy/thumb/{{ album.albumThumbnailAssetId }}"
|
||||
class="w-full h-full object-cover" alt="">
|
||||
</figure>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-start gap-2">
|
||||
<input type="checkbox" name="album_ids[]" value="{{ album.id }}"
|
||||
class="checkbox checkbox-primary mt-1">
|
||||
<div>
|
||||
<p class="font-semibold">{{ album.albumName }}</p>
|
||||
<p class="text-sm opacity-60">{{ album.assetCount }} photos</p>
|
||||
{% if album.has_state %}
|
||||
<span class="resume-badge badge badge-warning badge-sm mt-1">In progress</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4 max-w-xs">
|
||||
<label class="label"><span class="label-text">Grav trip slug</span></label>
|
||||
<input id="grav-slug" type="text" name="grav_trip_slug" required
|
||||
placeholder="central-asia-2023" class="input input-bordered">
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="start_over" id="start-over-flag" value="0">
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Start →</button>
|
||||
<button type="button" class="btn btn-ghost btn-sm"
|
||||
onclick="document.getElementById('start-over-flag').value='1'; this.closest('form').submit()">
|
||||
Start over (discard progress)
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
def test_album_list_renders(base_url, page):
|
||||
page.goto(base_url)
|
||||
assert page.locator(".album-card").count() == 1
|
||||
assert "Central Asia 2023" in page.inner_text(".album-card")
|
||||
|
||||
|
||||
def test_select_single_album_creates_state(base_url, page):
|
||||
page.goto(base_url)
|
||||
page.locator(".album-card input[type=checkbox]").first.check()
|
||||
page.fill("#grav-slug", "central-asia-2023")
|
||||
page.locator("button[type=submit]").click()
|
||||
page.wait_for_url("**/triage**")
|
||||
assert "album_id=album-1" in page.url
|
||||
|
||||
|
||||
def test_resume_prompt_shown_for_existing_state(base_url, page, seed_state):
|
||||
seed_state("phase2_state")
|
||||
page.goto(base_url)
|
||||
assert page.locator("[data-album-id=album-1] .resume-badge").is_visible()
|
||||
|
||||
|
||||
def test_immich_unreachable_shows_error(base_url, page, monkeypatch):
|
||||
import app.routes.albums as a
|
||||
orig = a.ImmichClient
|
||||
class BrokenClient:
|
||||
def __init__(self, *a, **k): pass
|
||||
def list_albums(self): raise ConnectionError("down")
|
||||
monkeypatch.setattr(a, "ImmichClient", BrokenClient)
|
||||
page.goto(base_url)
|
||||
assert page.locator(".alert-error").is_visible()
|
||||
Reference in New Issue
Block a user