|
1 | 1 | import { activeFilterChips, FILTER_DEFAULTS, FILTERS, gapIssueUrl, closestEntries, computeResults as computeResultsFor, passesFilters as passes, placementKey, queryTests as compileQuery, readState, scoreFields, searchFields, shouldGroup as shouldGroupFor, slugOf, sorterFor, SORTS, stateQuery, ENGINES } from "./search.js"; |
2 | 2 | import { CARD_LIMIT, LIST_LIMIT, entryCardHtml, escapeHtml, groupHeadingHtml, groupMoreHtml, PERSPECTIVE_LABELS } from "./cards.mjs"; |
3 | 3 | import { installViewToggles } from "./views.js"; |
| 4 | +import { catalogCsv } from "./exports.js"; |
4 | 5 | import { isListed, PERSPECTIVES } from "./shared.mjs"; |
5 | 6 |
|
6 | 7 | (() => { |
@@ -524,9 +525,13 @@ import { isListed, PERSPECTIVES } from "./shared.mjs"; |
524 | 525 | const SEARCH_DELAY = 120; |
525 | 526 | let searchTimer = 0; |
526 | 527 |
|
| 528 | + // The entries the current view shows, for "Download these" (#110). |
| 529 | + let lastList = []; |
| 530 | + |
527 | 531 | function apply({ push = false } = {}) { |
528 | 532 | clearTimeout(searchTimer); |
529 | 533 | const results = computeResults(); |
| 534 | + lastList = results.list; |
530 | 535 | syncCategoryChips(results); |
531 | 536 | renderActiveFilters(); |
532 | 537 | renderGrid(results); |
@@ -580,6 +585,39 @@ import { isListed, PERSPECTIVES } from "./shared.mjs"; |
580 | 585 |
|
581 | 586 | /* ----------------------------------------------------------- controls */ |
582 | 587 |
|
| 588 | + /** "Copy link to this view" and "Download these" beside the result count (#110). */ |
| 589 | + function bindViewActions() { |
| 590 | + const host = $("#view-actions"); |
| 591 | + if (!host) return; |
| 592 | + host.hidden = false; |
| 593 | + const status = $("#view-actions-status"); |
| 594 | + $("#copy-view-link").addEventListener("click", async () => { |
| 595 | + const url = location.href.split("#")[0] + "#catalog"; |
| 596 | + try { |
| 597 | + await navigator.clipboard.writeText(url); |
| 598 | + status.textContent = "Link copied"; |
| 599 | + } catch { |
| 600 | + // Clipboard refused: show the link so it can be copied by hand. |
| 601 | + status.textContent = url; |
| 602 | + } |
| 603 | + }); |
| 604 | + for (const btn of $$("[data-results-export]", host)) { |
| 605 | + btn.addEventListener("click", () => { |
| 606 | + const csv = btn.getAttribute("data-results-export") === "csv"; |
| 607 | + const text = csv ? catalogCsv(lastList, data.site?.siteUrl || "") : `${JSON.stringify(lastList, null, 2)}\n`; |
| 608 | + const url = URL.createObjectURL(new Blob([text], { type: csv ? "text/csv" : "application/json" })); |
| 609 | + const link = document.createElement("a"); |
| 610 | + link.href = url; |
| 611 | + link.download = csv ? "free-game-dev-assets.csv" : "free-game-dev-assets.json"; |
| 612 | + document.body.append(link); |
| 613 | + link.click(); |
| 614 | + link.remove(); |
| 615 | + setTimeout(() => URL.revokeObjectURL(url), 1000); |
| 616 | + status.textContent = `Downloaded ${lastList.length} ${lastList.length === 1 ? "entry" : "entries"}`; |
| 617 | + }); |
| 618 | + } |
| 619 | + } |
| 620 | + |
583 | 621 | function bindControls() { |
584 | 622 | const search = $("#search"); |
585 | 623 | search.value = state.q; |
@@ -808,6 +846,7 @@ import { isListed, PERSPECTIVES } from "./shared.mjs"; |
808 | 846 | adoptNodes(); |
809 | 847 | renderCategoryFilters(); |
810 | 848 | bindControls(); |
| 849 | + bindViewActions(); |
811 | 850 | syncControls(); |
812 | 851 | apply(); |
813 | 852 | restoreScroll(); |
|
0 commit comments