Rework library sync around a JSON document; add card filters
Sync
The previous design put the merge inside SQL: timestamp-stamping triggers,
a tombstone table, per-device SQLite snapshots, a device id in the database,
and row merges written as correlated-subquery UPDATEs. All of that schema
was inherited by desktop, and the merge was very hard to verify.
Replace it with a single JSON document plus a three-way merge in plain Rust.
Storing the last-read remote document as the merge base makes "who changed
what" and "what was deleted" derivable, which removes the tombstone table,
the updated_at columns and all eight triggers. It also makes a single shared
file safe: if a publish is lost the remote is unchanged, so the next merge
re-detects the local edits and republishes rather than reading their absence
as a deletion. That removes the need for per-device files and for device
identity entirely. Schema impact drops to one column, decks.uuid.
Conflicts resolve to "theirs", which is required for convergence rather than
chosen for taste — preferring ours has each device re-asserting its own value
forever. Every such choice is reported to the user.
SQLite becomes invisible: the working copy lives in the app data dir and the
user only ever names a .json file. There is always a document, defaulting to
private storage, so edits are never stranded with nowhere to be saved.
Pointing at a file that doesn't exist yet moves the current library there;
pointing at one that does adopts it. A converter brings a pre-document
library.db across.
Android reaches the folder through the Storage Access Framework rather than
MANAGE_EXTERNAL_STORAGE, so no storage permission is declared at all.
Filters
Track card origin in reference.db: Universes Beyond, digital-only and
Un-set cards, plus numeric power/toughness. Each origin flag is derived as
"every printing", not "any printing" — ordinary cards get reprinted into
those sets, and Lightning Bolt appearing in Marvel Commander must not hide
it. Non-numeric power ("*", "1+*") stays NULL so it drops out of range
filters instead of being compared as zero.
Also wire up filters over data already present: price range, format legality
(restricted counts as legal) and "can be a commander". All of these degrade
silently against an older reference.db rather than failing every query.
The shared criteria live in one component used by both filter panels, as a
compact row of icon toggles.
Card Search gains the Library tab's bulk selection: checkboxes in both views,
select-all, and a bulk add-to-deck with a single undo for the whole batch.
This commit is contained in:
@@ -784,6 +784,28 @@ input[type="checkbox"] {
|
||||
gap: var(--space-3);
|
||||
max-width: 480px;
|
||||
}
|
||||
/* The currently chosen synced folder. */
|
||||
.sync-folder {
|
||||
font-size: var(--fs-body);
|
||||
font-weight: 600;
|
||||
color: var(--text-strong);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
/* Outcome of the synced-folder self-test. Not a `.hint`: whether sync works is
|
||||
the point of the section, so it reads at body size rather than as
|
||||
de-emphasized supporting text. */
|
||||
.sync-ok,
|
||||
.sync-bad {
|
||||
font-size: var(--fs-body);
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.sync-ok {
|
||||
color: var(--success);
|
||||
}
|
||||
.sync-bad {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Typography helpers
|
||||
@@ -1695,6 +1717,65 @@ tbody tr:hover {
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
/* Compact icon filter toggles, following .color-toggles' visual grammar: dim
|
||||
when off, lit with an accent ring when on. Replaces a stack of labelled
|
||||
checkboxes that ate most of the filter panel's height — the explanations
|
||||
moved into tooltips. */
|
||||
.icon-toggles {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.icon-toggle {
|
||||
position: relative;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-elev);
|
||||
color: var(--text);
|
||||
opacity: 0.4;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.1s, box-shadow 0.1s, color 0.1s;
|
||||
}
|
||||
.icon-toggle:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
.icon-toggle.on {
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 0 2px var(--accent-hover);
|
||||
}
|
||||
/* Removing cards should not look like selecting them. */
|
||||
.icon-toggle.exclude.on {
|
||||
color: var(--danger);
|
||||
box-shadow: 0 0 0 2px var(--danger);
|
||||
}
|
||||
/* Diagonal bar across an active exclusion, so "hidden" reads without the
|
||||
tooltip. */
|
||||
.icon-toggle-slash {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
to bottom right,
|
||||
transparent calc(50% - 1px),
|
||||
currentColor calc(50% - 1px),
|
||||
currentColor calc(50% + 1px),
|
||||
transparent calc(50% + 1px)
|
||||
);
|
||||
}
|
||||
/* Separates the exclusions from the selecting filter beside them. */
|
||||
.icon-toggles-sep {
|
||||
width: 1px;
|
||||
align-self: stretch;
|
||||
margin: 2px 3px;
|
||||
background: var(--border-soft);
|
||||
}
|
||||
|
||||
.location-input {
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
|
||||
Reference in New Issue
Block a user