Import lists from CSV or text and match each entry
The list editor gets an Import button. A .csv/.tsv/.txt file or pasted text is parsed on the server: header columns (Title, Year, Artist or Director, Note, Rank), numbered lines and trailing "(1999)" years are understood, and Canon's own export round-trips. Each row is then searched with the list's provider and scored. A row is matched automatically only when one result fits exactly and nothing else fits as well; a year or artist in the file breaks ties, an exact title beats a bracketed version like "(Live)", and "Artist - Title" lines are tried both ways. Custom lists need a year or subtitle to be certain. Everything else is shown for review with a recommended pick, the other results, "Add as written", Skip, and a search-again box. Entries are added in file order after the existing ones, with any notes from the file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aadacaff95
commit
dadf835be3
10 files changed
+1131
No files matched your search
@@ -827,3 +827,44 @@ input[type="checkbox"] { accent-color: var(--accent); width: 18px; height: 18px;
|
||||
.backup-list .backup-when { flex: 1; min-width: 0; font-size: 14px; font-weight: 550; }
|
||||
.backup-list .backup-meta { display: block; color: var(--muted); font-size: 12.5px; font-weight: 400; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.backup-list .backup-actions { display: flex; gap: 2px; flex: none; }
|
||||
|
||||
/* ---------- import ---------- */
|
||||
.import-drop {
|
||||
display: flex; align-items: center; gap: 14px; padding: 18px; cursor: pointer;
|
||||
border: 1.5px dashed var(--border); border-radius: var(--radius); background: var(--surface);
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
.import-drop:hover, .import-drop.is-over { border-color: var(--accent); background: var(--accent-soft); }
|
||||
.import-drop > .icon { width: 24px; height: 24px; color: var(--accent); flex: none; }
|
||||
.import-drop-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||
.import-file { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
|
||||
.import-dialog { max-width: min(760px, calc(100vw - 32px)); width: 100%; }
|
||||
.import-dialog-inner { display: flex; flex-direction: column; max-height: calc(100dvh - 48px); }
|
||||
.import-body { display: flex; flex-direction: column; gap: 12px; overflow-y: auto; min-height: 0; }
|
||||
.import-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
||||
.import-summary { font-weight: 600; font-size: 14px; }
|
||||
.import-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
||||
.import-row { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; display: flex; flex-direction: column; gap: 10px; background: var(--surface); }
|
||||
.import-row.is-review, .import-row.is-none, .import-row.is-error { border-color: color-mix(in srgb, var(--warn) 45%, var(--border)); }
|
||||
.import-row-head { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||
.import-num { color: var(--faint); font-size: 13px; font-variant-numeric: tabular-nums; min-width: 2.5ch; text-align: right; }
|
||||
.import-src { flex: 1; min-width: 0; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.import-src b { font-weight: 600; }
|
||||
.badge--ok { background: color-mix(in srgb, var(--success) 16%, transparent); color: var(--success); }
|
||||
.badge .icon { width: 12px; height: 12px; }
|
||||
.import-chosen { display: flex; align-items: center; gap: 10px; justify-content: space-between; padding-left: calc(2.5ch + 10px); }
|
||||
.import-card { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||
.import-card .result-art { width: 38px; flex: none; }
|
||||
.import-card-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||
.import-card .result-title { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.import-opts { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 6px; }
|
||||
.import-opt { display: flex; align-items: center; gap: 10px; padding: 6px 8px; border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer; min-width: 0; }
|
||||
.import-opt:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
|
||||
.import-opt input { accent-color: var(--accent); margin: 0; flex: none; }
|
||||
.import-opt.is-new, .import-opt.is-skip { font-size: 13.5px; }
|
||||
.import-new { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.import-new .icon { width: 16px; height: 16px; color: var(--accent); flex: none; }
|
||||
.import-again { display: flex; align-items: center; gap: 8px; }
|
||||
.import-again > .icon { width: 16px; height: 16px; color: var(--faint); flex: none; }
|
||||
.import-row .import-again input[type="search"] { flex: 1; min-width: 0; height: 30px; font-size: 13px; }
|
||||
.dialog-foot > .alert { margin: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13px; }
|
||||
Reference in new issue
Block a user