add(ui): adding loading circle
This commit is contained in:
+14
-1
@@ -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 = '<span class="spinner"></span> 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 = `<span class="spinner"></span> 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 () => {
|
||||
: `<p class="error">✗ ${esc(r.path)}: ${esc(r.error)}</p>`
|
||||
)
|
||||
.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 ----
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user