add(ui): adding loading circle
This commit is contained in:
+14
-1
@@ -257,7 +257,9 @@ let scanItems = [];
|
|||||||
let missingReqs = [];
|
let missingReqs = [];
|
||||||
|
|
||||||
$("#scan-btn").addEventListener("click", async () => {
|
$("#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 {
|
try {
|
||||||
const split = $("#split-dirs").checked;
|
const split = $("#split-dirs").checked;
|
||||||
const [scan, reqs] = await Promise.all([
|
const [scan, reqs] = await Promise.all([
|
||||||
@@ -277,6 +279,8 @@ $("#scan-btn").addEventListener("click", async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
$("#scan-info").textContent = "";
|
$("#scan-info").textContent = "";
|
||||||
toast(err.message, true);
|
toast(err.message, true);
|
||||||
|
} finally {
|
||||||
|
scanBtn.disabled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -440,6 +444,9 @@ $("#import-btn").addEventListener("click", async () => {
|
|||||||
request_id: item.request_id,
|
request_id: item.request_id,
|
||||||
}));
|
}));
|
||||||
if (!items.length) { toast("Nichts ausgewählt", true); return; }
|
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 {
|
try {
|
||||||
const res = await api("/api/import", { method: "POST", body: JSON.stringify({ items }) });
|
const res = await api("/api/import", { method: "POST", body: JSON.stringify({ items }) });
|
||||||
$("#import-results").innerHTML = res.results
|
$("#import-results").innerHTML = res.results
|
||||||
@@ -449,8 +456,14 @@ $("#import-btn").addEventListener("click", async () => {
|
|||||||
: `<p class="error">✗ ${esc(r.path)}: ${esc(r.error)}</p>`
|
: `<p class="error">✗ ${esc(r.path)}: ${esc(r.error)}</p>`
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
|
const failed = res.results.filter((r) => !r.ok).length;
|
||||||
|
toast(failed ? `Import fertig, ${failed} Fehler` : "Import fertig", failed > 0);
|
||||||
$("#scan-btn").click();
|
$("#scan-btn").click();
|
||||||
} catch (err) { toast(err.message, true); }
|
} catch (err) { toast(err.message, true); }
|
||||||
|
finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = "Ausgewählte importieren";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- init ----
|
// ---- 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 { 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::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); }
|
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-dialog { min-width: 480px; max-width: 640px; }
|
||||||
#quick-results { max-height: 50vh; overflow-y: auto; margin-top: 0.6rem; }
|
#quick-results { max-height: 50vh; overflow-y: auto; margin-top: 0.6rem; }
|
||||||
.ok { color: #6fbf73; }
|
.ok { color: #6fbf73; }
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class BookRequest(Base):
|
|||||||
library_id: Mapped[int] = mapped_column(ForeignKey("libraries.id"))
|
library_id: Mapped[int] = mapped_column(ForeignKey("libraries.id"))
|
||||||
title: Mapped[str] = mapped_column(String)
|
title: Mapped[str] = mapped_column(String)
|
||||||
authors: Mapped[str] = mapped_column(String, default="") # comma-separated
|
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
|
external_id: Mapped[str] = mapped_column(String, default="") # ISBN / ASIN / AniList id
|
||||||
year: Mapped[int | None] = mapped_column(default=None)
|
year: Mapped[int | None] = mapped_column(default=None)
|
||||||
series: Mapped[str] = mapped_column(String, default="")
|
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)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
_engine = create_engine(f"sqlite:///{path}", connect_args={"check_same_thread": False})
|
_engine = create_engine(f"sqlite:///{path}", connect_args={"check_same_thread": False})
|
||||||
Base.metadata.create_all(_engine)
|
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)
|
SessionLocal = sessionmaker(bind=_engine, expire_on_commit=False)
|
||||||
return _engine
|
return _engine
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user