diff --git a/static/app.js b/static/app.js index e28c374..dfd4a7e 100644 --- a/static/app.js +++ b/static/app.js @@ -257,7 +257,9 @@ let scanItems = []; let missingReqs = []; $("#scan-btn").addEventListener("click", async () => { - $("#scan-info").textContent = "Scanne…"; + const scanBtn = $("#scan-btn"); + scanBtn.disabled = true; + $("#scan-info").innerHTML = ' Scanne…'; try { const split = $("#split-dirs").checked; const [scan, reqs] = await Promise.all([ @@ -277,6 +279,8 @@ $("#scan-btn").addEventListener("click", async () => { } catch (err) { $("#scan-info").textContent = ""; toast(err.message, true); + } finally { + scanBtn.disabled = false; } }); @@ -440,6 +444,9 @@ $("#import-btn").addEventListener("click", async () => { request_id: item.request_id, })); if (!items.length) { toast("Nichts ausgewählt", true); return; } + const btn = $("#import-btn"); + btn.disabled = true; + btn.innerHTML = ` Importiere ${items.length} Einträge…`; try { const res = await api("/api/import", { method: "POST", body: JSON.stringify({ items }) }); $("#import-results").innerHTML = res.results @@ -449,8 +456,14 @@ $("#import-btn").addEventListener("click", async () => { : `

✗ ${esc(r.path)}: ${esc(r.error)}

` ) .join(""); + const failed = res.results.filter((r) => !r.ok).length; + toast(failed ? `Import fertig, ${failed} Fehler` : "Import fertig", failed > 0); $("#scan-btn").click(); } catch (err) { toast(err.message, true); } + finally { + btn.disabled = false; + btn.textContent = "Ausgewählte importieren"; + } }); // ---- init ---- diff --git a/static/style.css b/static/style.css index 6ebd688..ac3010b 100644 --- a/static/style.css +++ b/static/style.css @@ -39,6 +39,12 @@ td select { max-width: 340px; } dialog { background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 1.2rem; min-width: 340px; } dialog::backdrop { background: rgba(0,0,0,0.6); } dialog label { display: flex; flex-direction: column; gap: 0.2rem; margin: 0.5rem 0; font-size: 0.9rem; color: var(--muted); } +.spinner { + display: inline-block; width: 0.9em; height: 0.9em; vertical-align: -0.12em; + border: 2px solid rgba(255,255,255,0.35); border-top-color: #fff; + border-radius: 50%; animation: spin 0.8s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } #quick-dialog { min-width: 480px; max-width: 640px; } #quick-results { max-height: 50vh; overflow-y: auto; margin-top: 0.6rem; } .ok { color: #6fbf73; } diff --git a/wordarr/db.py b/wordarr/db.py index f3b260d..6299fc1 100644 --- a/wordarr/db.py +++ b/wordarr/db.py @@ -31,6 +31,7 @@ class BookRequest(Base): library_id: Mapped[int] = mapped_column(ForeignKey("libraries.id")) title: Mapped[str] = mapped_column(String) authors: Mapped[str] = mapped_column(String, default="") # comma-separated + narrator: Mapped[str] = mapped_column(String, default="") external_id: Mapped[str] = mapped_column(String, default="") # ISBN / ASIN / AniList id year: Mapped[int | None] = mapped_column(default=None) series: Mapped[str] = mapped_column(String, default="") @@ -55,6 +56,11 @@ def init_db(db_path=None): path.parent.mkdir(parents=True, exist_ok=True) _engine = create_engine(f"sqlite:///{path}", connect_args={"check_same_thread": False}) Base.metadata.create_all(_engine) + # lightweight migration: create_all doesn't add new columns to existing tables + with _engine.begin() as conn: + cols = [row[1] for row in conn.exec_driver_sql("PRAGMA table_info(requests)")] + if "narrator" not in cols: + conn.exec_driver_sql("ALTER TABLE requests ADD COLUMN narrator VARCHAR NOT NULL DEFAULT ''") SessionLocal = sessionmaker(bind=_engine, expire_on_commit=False) return _engine