Pace IGDB requests; rewrite the new help text
IGDB allows 4 requests a second, and an import searching three entries at once (plus detail lookups) went over it. Every IGDB request now waits at least 300 ms after the last one, and a 429 is retried after 1, 2 and 4 seconds before giving up. The import dialog, ranking editor and their error messages are reworded in plainer language, and talk about "spots" instead of "places". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
62fcf6b45d
commit
1f3fa4fb2b
7 files changed
+55
-32
No files matched your search
@@ -99,11 +99,15 @@ func (g *igdb) query(ctx context.Context, body string) ([]Result, error) {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
} `json:"platforms"`
|
} `json:"platforms"`
|
||||||
}
|
}
|
||||||
for attempt := 0; ; attempt++ {
|
refreshed, backoff := false, time.Second
|
||||||
tok, err := g.token(ctx, attempt > 0)
|
for {
|
||||||
|
tok, err := g.token(ctx, refreshed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := g.r.igdbPace.wait(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.igdb.com/v4/games", strings.NewReader(body))
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.igdb.com/v4/games", strings.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -112,7 +116,19 @@ func (g *igdb) query(ctx context.Context, body string) ([]Result, error) {
|
|||||||
req.Header.Set("Authorization", "Bearer "+tok)
|
req.Header.Set("Authorization", "Bearer "+tok)
|
||||||
req.Header.Set("Content-Type", "text/plain")
|
req.Header.Set("Content-Type", "text/plain")
|
||||||
err = g.r.do(req, &games)
|
err = g.r.do(req, &games)
|
||||||
if isStatus(err, 401) && attempt == 0 {
|
if isStatus(err, 401) && !refreshed {
|
||||||
|
refreshed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Rate limited anyway (another app on the same key, say): back off
|
||||||
|
// and try again, up to about 7 seconds in all.
|
||||||
|
if isStatus(err, 429) && backoff <= 4*time.Second {
|
||||||
|
select {
|
||||||
|
case <-time.After(backoff):
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
backoff *= 2
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ type Registry struct {
|
|||||||
tvdb *tvdbTokens
|
tvdb *tvdbTokens
|
||||||
igdb *igdbTokens
|
igdb *igdbTokens
|
||||||
mb *throttle
|
mb *throttle
|
||||||
|
// IGDB allows 4 requests a second; stay under it so an import's
|
||||||
|
// searches and detail lookups don't get answered with 429.
|
||||||
|
igdbPace *throttle
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRegistry(creds func(context.Context) Credentials) *Registry {
|
func NewRegistry(creds func(context.Context) Credentials) *Registry {
|
||||||
@@ -83,6 +86,7 @@ func NewRegistry(creds func(context.Context) Credentials) *Registry {
|
|||||||
tvdb: &tvdbTokens{},
|
tvdb: &tvdbTokens{},
|
||||||
igdb: &igdbTokens{},
|
igdb: &igdbTokens{},
|
||||||
mb: &throttle{gap: 1100 * time.Millisecond},
|
mb: &throttle{gap: 1100 * time.Millisecond},
|
||||||
|
igdbPace: &throttle{gap: 300 * time.Millisecond},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ func (s *Store) UpdateList(ctx context.Context, l List) (List, error) {
|
|||||||
if last, err := s.MaxRank(ctx, l.ID); err != nil {
|
if last, err := s.MaxRank(ctx, l.ID); err != nil {
|
||||||
return l, err
|
return l, err
|
||||||
} else if l.MaxItems < last {
|
} else if l.MaxItems < last {
|
||||||
return l, invalid("An entry sits at #%d; move it up or close the gaps before shrinking the list to %d", last, l.MaxItems)
|
return l, invalid("Something's at #%d, so the list can't be smaller than that. Move it up or close the gaps first.", last)
|
||||||
}
|
}
|
||||||
if l.Category != cur.Category && cur.ItemCount > 0 {
|
if l.Category != cur.Category && cur.ItemCount > 0 {
|
||||||
return l, invalid("Can't change the category of a list that has items")
|
return l, invalid("Can't change the category of a list that has items")
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func (p places) makeRoom(at, limit int) error {
|
|||||||
gap++
|
gap++
|
||||||
}
|
}
|
||||||
if gap > limit {
|
if gap > limit {
|
||||||
return invalid("No room: this list ends at #%d. Raise its size in List settings, or remove an empty place first.", limit)
|
return invalid("There's no room left. This list stops at #%d; make it bigger in List settings or close up an open spot.", limit)
|
||||||
}
|
}
|
||||||
p.shift(at, gap-1, +1)
|
p.shift(at, gap-1, +1)
|
||||||
return nil
|
return nil
|
||||||
@@ -187,7 +187,7 @@ func (s *Store) beginPlaces(ctx context.Context, listID int64) (*placeTx, error)
|
|||||||
// write saves the places that changed, without committing.
|
// write saves the places that changed, without committing.
|
||||||
func (pt *placeTx) write(ctx context.Context) error {
|
func (pt *placeTx) write(ctx context.Context) error {
|
||||||
if last := pt.p.last(); last > pt.limit {
|
if last := pt.p.last(); last > pt.limit {
|
||||||
return invalid("No room: this list ends at #%d. Raise its size in List settings.", pt.limit)
|
return invalid("There's no room left. This list stops at #%d; make it bigger in List settings.", pt.limit)
|
||||||
}
|
}
|
||||||
stmt, err := pt.tx.PrepareContext(ctx, `UPDATE items SET rank = ? WHERE id = ?`)
|
stmt, err := pt.tx.PrepareContext(ctx, `UPDATE items SET rank = ? WHERE id = ?`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -241,7 +241,7 @@ func (pt *placeTx) list() []Place {
|
|||||||
|
|
||||||
func (pt *placeTx) checkPlace(at int) error {
|
func (pt *placeTx) checkPlace(at int) error {
|
||||||
if at < 1 || at > pt.limit {
|
if at < 1 || at > pt.limit {
|
||||||
return invalid("Pick a place from #1 to #%d.", pt.limit)
|
return invalid("Pick a number from 1 to %d.", pt.limit)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -310,7 +310,7 @@ func (s *Store) RemovePlace(ctx context.Context, listID int64, at, count int) ([
|
|||||||
count = max(count, 1)
|
count = max(count, 1)
|
||||||
for r := at; r < at+count; r++ {
|
for r := at; r < at+count; r++ {
|
||||||
if _, taken := pt.p[r]; taken {
|
if _, taken := pt.p[r]; taken {
|
||||||
return nil, invalid("#%d isn't empty.", r)
|
return nil, invalid("#%d isn't open.", r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for range count {
|
for range count {
|
||||||
|
|||||||
+12
-12
@@ -138,7 +138,7 @@ function syncRanks() {
|
|||||||
function moveTo(it, to, flash = true) {
|
function moveTo(it, to, flash = true) {
|
||||||
to = Math.round(to);
|
to = Math.round(to);
|
||||||
if (!Number.isFinite(to) || to < 1 || to > list.maxItems) {
|
if (!Number.isFinite(to) || to < 1 || to > list.maxItems) {
|
||||||
toast(`Pick a place from #1 to #${fmt.format(list.maxItems)}.`, { error: true });
|
toast(`Pick a number from 1 to ${fmt.format(list.maxItems)}.`, { error: true });
|
||||||
render();
|
render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ function moveTo(it, to, flash = true) {
|
|||||||
|
|
||||||
function insertPlace(at) {
|
function insertPlace(at) {
|
||||||
if (!places.makeRoom(at)) {
|
if (!places.makeRoom(at)) {
|
||||||
toast(`No room: this list ends at #${fmt.format(list.maxItems)}. Raise its size in List settings.`, { error: true });
|
toast(`There's no room left. This list stops at #${fmt.format(list.maxItems)}; you can make it bigger in List settings.`, { error: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
@@ -180,8 +180,8 @@ async function removeItem(it) {
|
|||||||
if (places.at.has(r + 1)) {
|
if (places.at.has(r + 1)) {
|
||||||
const choice = await chooseDialog({
|
const choice = await chooseDialog({
|
||||||
title: `Remove “${it.title}”?`,
|
title: `Remove “${it.title}”?`,
|
||||||
message: `It's #${r}. Should the entries below it move up, or should #${r} stay empty?`,
|
message: `It's at #${r}. Want everything below it to move up, or keep #${r} open?`,
|
||||||
choices: [{ label: `Leave #${r} empty`, value: "keep" }, { label: "Move them up", value: "close", primary: true }],
|
choices: [{ label: `Keep #${r} open`, value: "keep" }, { label: "Move them up", value: "close", primary: true }],
|
||||||
});
|
});
|
||||||
if (!choice) return;
|
if (!choice) return;
|
||||||
keep = choice === "keep";
|
keep = choice === "keep";
|
||||||
@@ -230,7 +230,7 @@ function itemRow(it) {
|
|||||||
function buildRow(it) {
|
function buildRow(it) {
|
||||||
const input = h("input", {
|
const input = h("input", {
|
||||||
class: "rank-input", type: "text", inputmode: "numeric", autocomplete: "off", value: it.rank,
|
class: "rank-input", type: "text", inputmode: "numeric", autocomplete: "off", value: it.rank,
|
||||||
"aria-label": `Place of ${it.title}`, title: "Type a place and press Enter. ↑/↓ go to the next entry; Alt+↑/↓ moves this one.",
|
"aria-label": `Spot for ${it.title}`, title: "Type a number and hit Enter. Alt + arrow keys nudge it up or down.",
|
||||||
});
|
});
|
||||||
const commit = () => {
|
const commit = () => {
|
||||||
// A redraw detaching the box fires blur; that isn't the owner leaving it.
|
// A redraw detaching the box fires blur; that isn't the owner leaving it.
|
||||||
@@ -268,7 +268,7 @@ function buildRow(it) {
|
|||||||
const menu = h("details", { class: "menu row-menu" },
|
const menu = h("details", { class: "menu row-menu" },
|
||||||
h("summary", { class: "icon-btn", "aria-label": `More for ${it.title}`, title: "More" }, icon("chevron-down")),
|
h("summary", { class: "icon-btn", "aria-label": `More for ${it.title}`, title: "More" }, icon("chevron-down")),
|
||||||
h("div", { class: "menu-pop" },
|
h("div", { class: "menu-pop" },
|
||||||
h("button", { type: "button", onclick: () => insertPlace(it.rank) }, icon("plus"), "Insert an empty place above"),
|
h("button", { type: "button", onclick: () => insertPlace(it.rank) }, icon("plus"), "Open up a spot above"),
|
||||||
h("button", { type: "button", onclick: () => moveTo(it, 1) }, icon("chevron-up"), "Move to #1"),
|
h("button", { type: "button", onclick: () => moveTo(it, 1) }, icon("chevron-up"), "Move to #1"),
|
||||||
h("button", { type: "button", onclick: () => moveTo(it, places.last()) }, icon("chevron-down"), "Move to the end"),
|
h("button", { type: "button", onclick: () => moveTo(it, places.last()) }, icon("chevron-down"), "Move to the end"),
|
||||||
canRefresh(it) && h("button", { type: "button", onclick: (e) => refreshOne(it, e.currentTarget) }, icon("refresh"), `Refresh from ${sourceName(it)}`),
|
canRefresh(it) && h("button", { type: "button", onclick: (e) => refreshOne(it, e.currentTarget) }, icon("refresh"), `Refresh from ${sourceName(it)}`),
|
||||||
@@ -305,12 +305,12 @@ function emptyRow(from, to) {
|
|||||||
const label = n === 1 ? `#${fmt.format(from)}` : `#${fmt.format(from)}–${fmt.format(to)}`;
|
const label = n === 1 ? `#${fmt.format(from)}` : `#${fmt.format(from)}–${fmt.format(to)}`;
|
||||||
const targeted = addAt >= from && addAt <= to;
|
const targeted = addAt >= from && addAt <= to;
|
||||||
return h("li", { class: "rank-gap" + (targeted ? " is-target" : ""), "data-place": from, "data-place-end": to },
|
return h("li", { class: "rank-gap" + (targeted ? " is-target" : ""), "data-place": from, "data-place-end": to },
|
||||||
h("span", { class: "rank-gap-label" }, h("b", {}, label), n === 1 ? " Empty place" : ` ${fmt.format(n)} empty places`),
|
h("span", { class: "rank-gap-label" }, h("b", {}, label), n === 1 ? " Open spot" : ` ${fmt.format(n)} open spots`),
|
||||||
h("span", { class: "rank-gap-actions" },
|
h("span", { class: "rank-gap-actions" },
|
||||||
h("button", { type: "button", class: "btn btn--sm btn--ghost", onclick: () => setAddAt(targeted ? 0 : from) },
|
h("button", { type: "button", class: "btn btn--sm btn--ghost", onclick: () => setAddAt(targeted ? 0 : from) },
|
||||||
icon(targeted ? "x" : "plus"), targeted ? "Cancel" : "Add here"),
|
icon(targeted ? "x" : "plus"), targeted ? "Cancel" : "Add here"),
|
||||||
h("button", { type: "button", class: "btn btn--sm btn--ghost", title: "Remove these places; everything below moves up", onclick: () => removePlaces(from, n) },
|
h("button", { type: "button", class: "btn btn--sm btn--ghost", title: "Close this up; everything below moves up", onclick: () => removePlaces(from, n) },
|
||||||
icon("trash"), n === 1 ? "Remove place" : "Remove places"),
|
icon("trash"), "Close up"),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -319,7 +319,7 @@ function render(flashId) {
|
|||||||
syncRanks();
|
syncRanks();
|
||||||
const gaps = places.last() - places.at.size;
|
const gaps = places.last() - places.at.size;
|
||||||
rankTools.hidden = items.length === 0;
|
rankTools.hidden = items.length === 0;
|
||||||
gapInfo.textContent = gaps ? `${fmt.format(gaps)} empty ${gaps === 1 ? "place" : "places"}` : "No gaps";
|
gapInfo.textContent = gaps ? `${fmt.format(gaps)} open ${gaps === 1 ? "spot" : "spots"}` : "No gaps";
|
||||||
closeGapsBtn.hidden = gaps === 0;
|
closeGapsBtn.hidden = gaps === 0;
|
||||||
renderAddTarget();
|
renderAddTarget();
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
@@ -393,7 +393,7 @@ function advanceAddAt(added) {
|
|||||||
|
|
||||||
const rankTools = h("div", { class: "rank-tools", hidden: true });
|
const rankTools = h("div", { class: "rank-tools", hidden: true });
|
||||||
const gapInfo = h("span", { class: "muted small" });
|
const gapInfo = h("span", { class: "muted small" });
|
||||||
const closeGapsBtn = h("button", { type: "button", class: "btn btn--sm btn--ghost", title: "Number the entries 1, 2, 3… keeping their order", onclick: closeGaps }, "Close gaps");
|
const closeGapsBtn = h("button", { type: "button", class: "btn btn--sm btn--ghost", title: "Renumber everything 1, 2, 3… in the same order", onclick: closeGaps }, "Close gaps");
|
||||||
const jump = h("input", { type: "text", inputmode: "numeric", placeholder: "Go to #", "aria-label": "Go to place", class: "rank-jump" });
|
const jump = h("input", { type: "text", inputmode: "numeric", placeholder: "Go to #", "aria-label": "Go to place", class: "rank-jump" });
|
||||||
jump.addEventListener("keydown", (e) => {
|
jump.addEventListener("keydown", (e) => {
|
||||||
if (e.key !== "Enter") return;
|
if (e.key !== "Enter") return;
|
||||||
@@ -634,7 +634,7 @@ function renderResults(res) {
|
|||||||
action,
|
action,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (full) nodes.unshift(h("p", { class: "alert alert--warn" }, icon("info"), `This list is full (${fmt.format(list.maxItems)}). Raise its size in List settings to add more.`));
|
if (full) nodes.unshift(h("p", { class: "alert alert--warn" }, icon("info"), `This list is full at ${fmt.format(list.maxItems)}. Make it bigger in List settings to add more.`));
|
||||||
results.replaceChildren(...nodes);
|
results.replaceChildren(...nodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-11
@@ -8,15 +8,15 @@ const CONCURRENCY = 3;
|
|||||||
export function openImport({ list, cat, onListChange, onDone }) {
|
export function openImport({ list, cat, onListChange, onDone }) {
|
||||||
let dlg;
|
let dlg;
|
||||||
const file = h("input", { type: "file", accept: ".csv,.tsv,.txt,text/csv,text/plain,text/tab-separated-values", class: "import-file" });
|
const file = h("input", { type: "file", accept: ".csv,.tsv,.txt,text/csv,text/plain,text/tab-separated-values", class: "import-file" });
|
||||||
const text = h("textarea", { rows: 6, placeholder: "Or paste titles here, one per line", "aria-label": "Titles to import" });
|
const text = h("textarea", { rows: 6, placeholder: "Or paste your list here", "aria-label": "Titles to import" });
|
||||||
const error = h("p", { class: "alert alert--error", role: "alert", hidden: true });
|
const error = h("p", { class: "alert alert--error", role: "alert", hidden: true });
|
||||||
const next = h("button", { type: "submit", class: "btn btn--primary" }, "Find matches");
|
const next = h("button", { type: "submit", class: "btn btn--primary" }, "Find matches");
|
||||||
const fileLabel = h("span", { class: "import-drop-text" }, h("b", {}, "Choose a .csv or .txt file"), h("span", { class: "muted small" }, "or drop it here"));
|
const fileLabel = h("span", { class: "import-drop-text" }, h("b", {}, "Choose a .txt or .csv file"), h("span", { class: "muted small" }, "or drag it here"));
|
||||||
const drop = h("label", { class: "import-drop" }, icon("upload"), fileLabel, file);
|
const drop = h("label", { class: "import-drop" }, icon("upload"), fileLabel, file);
|
||||||
|
|
||||||
file.addEventListener("change", () => {
|
file.addEventListener("change", () => {
|
||||||
const f = file.files[0];
|
const f = file.files[0];
|
||||||
fileLabel.replaceChildren(h("b", {}, f ? f.name : "Choose a .csv or .txt file"), h("span", { class: "muted small" }, f ? `${Math.ceil(f.size / 1024)} KB · click to change` : "or drop it here"));
|
fileLabel.replaceChildren(h("b", {}, f ? f.name : "Choose a .txt or .csv file"), h("span", { class: "muted small" }, f ? `${Math.ceil(f.size / 1024)} KB · click to pick another` : "or drag it here"));
|
||||||
});
|
});
|
||||||
for (const ev of ["dragenter", "dragover"]) drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.add("is-over"); });
|
for (const ev of ["dragenter", "dragover"]) drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.add("is-over"); });
|
||||||
for (const ev of ["dragleave", "drop"]) drop.addEventListener(ev, () => drop.classList.remove("is-over"));
|
for (const ev of ["dragleave", "drop"]) drop.addEventListener(ev, () => drop.classList.remove("is-over"));
|
||||||
@@ -31,9 +31,12 @@ export function openImport({ list, cat, onListChange, onDone }) {
|
|||||||
const form = h("form", {},
|
const form = h("form", {},
|
||||||
dialogHead("Import a list", () => dlg),
|
dialogHead("Import a list", () => dlg),
|
||||||
h("div", { class: "dialog-body stack" },
|
h("div", { class: "dialog-body stack" },
|
||||||
h("p", { class: "muted" },
|
h("p", {},
|
||||||
`Line 1 becomes #1, line 38 becomes #38, and a blank line leaves its place empty${list.itemCount ? " (entries already at those places move down to make room)" : ""}. Use one title per line, or a CSV with columns like Title, Year, ${artistWord(cat)} and Note. `,
|
"Put one title on each line. The line number is the spot on your list, so line 1 is #1 and line 38 is #38. Leave a line blank to keep that spot open.",
|
||||||
"Each one is looked up and matched for you; anything Canon isn't sure about is left for you to check."),
|
list.itemCount ? " Anything already in those spots gets bumped down." : ""),
|
||||||
|
h("p", { class: "muted small" },
|
||||||
|
`Using a spreadsheet? Columns called Title, Year, ${artistWord(cat)} and Note are picked up automatically. `,
|
||||||
|
"Canon looks everything up for you and only asks about the ones it isn't sure of."),
|
||||||
drop,
|
drop,
|
||||||
text,
|
text,
|
||||||
error,
|
error,
|
||||||
@@ -85,7 +88,7 @@ export function openImport({ list, cat, onListChange, onDone }) {
|
|||||||
const summary = h("p", { class: "import-summary", "aria-live": "polite" });
|
const summary = h("p", { class: "import-summary", "aria-live": "polite" });
|
||||||
const filter = h("label", { class: "check small" }, h("input", {
|
const filter = h("label", { class: "check small" }, h("input", {
|
||||||
type: "checkbox", onchange: (e) => { onlyCheck = e.target.checked; renderAll(); },
|
type: "checkbox", onchange: (e) => { onlyCheck = e.target.checked; renderAll(); },
|
||||||
}), "Show only ones to check");
|
}), "Only show the ones to check");
|
||||||
const listEl = h("ol", { class: "import-list" });
|
const listEl = h("ol", { class: "import-list" });
|
||||||
const room = h("p", { class: "alert alert--warn", hidden: true });
|
const room = h("p", { class: "alert alert--warn", hidden: true });
|
||||||
const go = h("button", { type: "button", class: "btn btn--primary", disabled: true }, "Import");
|
const go = h("button", { type: "button", class: "btn btn--primary", disabled: true }, "Import");
|
||||||
@@ -127,8 +130,8 @@ export function openImport({ list, cat, onListChange, onDone }) {
|
|||||||
if (!room.hidden) {
|
if (!room.hidden) {
|
||||||
const want = Math.min(Math.max(list.itemCount + n, lastPlace), 65535);
|
const want = Math.min(Math.max(list.itemCount + n, lastPlace), 65535);
|
||||||
room.replaceChildren(icon("info"), lastPlace > list.maxItems
|
room.replaceChildren(icon("info"), lastPlace > list.maxItems
|
||||||
? `The file goes up to #${fmt.format(lastPlace)}, but this list ends at #${fmt.format(list.maxItems)}. `
|
? `Your file goes up to #${fmt.format(lastPlace)}, but this list stops at #${fmt.format(list.maxItems)}. `
|
||||||
: `This list has room for ${fmt.format(list.maxItems - list.itemCount)} more. `,
|
: `There's only room for ${fmt.format(list.maxItems - list.itemCount)} more. `,
|
||||||
h("button", {
|
h("button", {
|
||||||
type: "button", class: "btn btn--sm", onclick: async (e) => {
|
type: "button", class: "btn btn--sm", onclick: async (e) => {
|
||||||
busy(e.currentTarget, true);
|
busy(e.currentTarget, true);
|
||||||
@@ -138,7 +141,7 @@ export function openImport({ list, cat, onListChange, onDone }) {
|
|||||||
renderTop();
|
renderTop();
|
||||||
} catch (err) { toast(err.message, { error: true }); busy(e.currentTarget, false); }
|
} catch (err) { toast(err.message, { error: true }); busy(e.currentTarget, false); }
|
||||||
},
|
},
|
||||||
}, `Raise the size to ${fmt.format(want)}`));
|
}, `Make it a Top ${fmt.format(want)}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +194,7 @@ export function openImport({ list, cat, onListChange, onDone }) {
|
|||||||
content);
|
content);
|
||||||
const opts = e.results.map((r, idx) => option(idx, resultCard(r, idx === 0 && e.state !== "none" ? "Recommended" : "")));
|
const opts = e.results.map((r, idx) => option(idx, resultCard(r, idx === 0 && e.state !== "none" ? "Recommended" : "")));
|
||||||
opts.push(option("manual", h("span", { class: "import-new" }, icon("plus"), h("span", {}, "Add ", h("b", {}, `“${row.title}”`), " as written")), "is-new"));
|
opts.push(option("manual", h("span", { class: "import-new" }, icon("plus"), h("span", {}, "Add ", h("b", {}, `“${row.title}”`), " as written")), "is-new"));
|
||||||
opts.push(option("skip", h("span", { class: "muted" }, "Skip it"), "is-skip"));
|
opts.push(option("skip", h("span", { class: "muted" }, "Skip"), "is-skip"));
|
||||||
|
|
||||||
const q = h("input", { type: "search", value: row.title, "aria-label": `Search again for ${row.title}` });
|
const q = h("input", { type: "search", value: row.title, "aria-label": `Search again for ${row.title}` });
|
||||||
const again = h("form", { class: "import-again" }, icon("search"), q,
|
const again = h("form", { class: "import-again" }, icon("search"), q,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<section class="panel editor-items" aria-labelledby="items-h">
|
<section class="panel editor-items" aria-labelledby="items-h">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<h2 id="items-h">Ranking</h2>
|
<h2 id="items-h">Ranking</h2>
|
||||||
<span class="muted small">Drag, or type a place and press Enter. Places can be left empty.</span>
|
<span class="muted small">Drag to reorder, or type a number. Spots can stay open.</span>
|
||||||
</div>
|
</div>
|
||||||
<ol class="rank-list" data-rank-list></ol>
|
<ol class="rank-list" data-rank-list></ol>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in new issue
Block a user