pmtiles-swarm 0.46.0 โ†’ 0.47.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@
7
7
  ### ๐Ÿž Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.47.1
11
+ ### โœจ Features and improvements
12
+
13
+ ### ๐Ÿž Bug fixes
14
+ - **The magnet button copies a magnet, not a link to one.** `/latest/<category>/magnet` answers a
15
+ magnet URI as `text/plain`, and both pages were copying that endpoint's own address โ€” so what
16
+ landed on the clipboard was an `http://` URL, which a torrent client cannot open. The button now
17
+ fetches the endpoint and copies what it answers. Every other endpoint is still copied by address,
18
+ because for those the address is the thing.
19
+
20
+ ## 0.47.0
21
+ ### โœจ Features and improvements
22
+ - **The console's Categories tab now matches the public page.** They were two views of one thing that
23
+ had drifted into two shapes: a label/value table with Copy and Open on every row here, links and a
24
+ printed style URL there, different labels for the same endpoints, and a preview link on one and not
25
+ the other.
26
+
27
+ Both now offer each endpoint the way it is used โ€” `.torrent` downloads, preview opens, and
28
+ TileJSON, magnet, RSS and the style URL copy. The console keeps `RSS, newest only`, which is its
29
+ own, and gains the preview link it was missing. The style URL is no longer printed on either page:
30
+ here it was truncated to 96 characters, which is long enough to fill the row and too short to be
31
+ the thing anybody wanted.
32
+
33
+ ### ๐Ÿž Bug fixes
34
+
10
35
  ## 0.46.0
11
36
  ### โœจ Features and improvements
12
37
  - **Each category endpoint on the public page is now offered the way it is used.** A `.torrent` is a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.46.0",
3
+ "version": "0.47.1",
4
4
  "description": "BitTorrent distribution for PMTiles map archives: create torrents, watch folders, publish and subscribe to RSS feeds, and seed through qBittorrent or an embedded client",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -224,6 +224,23 @@
224
224
  }
225
225
  .tag.on { background: var(--accent); color: #fff; border-color: var(--accent); }
226
226
  #add-error { color: var(--bad); min-height: 1.2rem; }
227
+ /* One row of endpoints per category, matching the public page. */
228
+ #category-list .links {
229
+ display: flex;
230
+ flex-wrap: wrap;
231
+ gap: 0.4rem 0.9rem;
232
+ align-items: center;
233
+ }
234
+ /* A copy button reads as a link's sibling, not as a form control. */
235
+ button.copy {
236
+ border: 1px solid var(--line);
237
+ border-radius: 999px;
238
+ background: none;
239
+ color: var(--muted);
240
+ font-size: 0.74rem;
241
+ padding: 0 0.45rem;
242
+ }
243
+ button.copy:hover { color: var(--fg); border-color: var(--fg); }
227
244
  /* Category endpoints. `th, td` is nowrap for the archive table, where a
228
245
  wrapped number is worse than a wide one โ€” but here the cell holds a
229
246
  style URL and a paragraph explaining it, so nowrap pushed the table
@@ -3477,20 +3494,40 @@ Every piece is hashed against the ` +
3477
3494
  list.innerHTML = categories
3478
3495
  .map((entry, index) => {
3479
3496
  const newest = entry.newest;
3480
- const rows = [
3481
- ['TileJSON', entry.endpoints.tileJson],
3482
- ['For a style', entry.endpoints.styleUrl],
3483
- ['Torrent', entry.endpoints.torrent],
3484
- ['Magnet', entry.endpoints.magnet],
3485
- ['Feed', entry.endpoints.feed],
3486
- ['Latest only', entry.endpoints.latestFeed],
3487
- ];
3497
+ // The same shape as the public page, and for the same reason: what
3498
+ // an endpoint is for decides how it is offered. A `.torrent` is a
3499
+ // file, so it downloads; a preview is a page, so it opens. The
3500
+ // rest are addresses that belong somewhere else โ€” a style, a
3501
+ // torrent client, a feed reader โ€” and following one from here
3502
+ // achieves nothing, so they copy.
3503
+ //
3504
+ // The style URL is not printed. It is a TileJSON URL carrying a
3505
+ // `.torrent` URL and a percent-encoded magnet in its fragment,
3506
+ // several hundred characters of it, and it was truncated to 96
3507
+ // here โ€” long enough to fill the row and too short to be the
3508
+ // thing anybody wanted.
3509
+ const ends = entry.endpoints;
3510
+ const link = (url, text, download) =>
3511
+ url
3512
+ ? `<a href="${escapeHtml(url)}"${download ? ' download' : ' target="_blank" rel="noreferrer"'}>${text}</a>`
3513
+ : '';
3514
+ // `fetched` for an endpoint whose *answer* is the thing worth
3515
+ // having rather than its address. /magnet serves a magnet URI as
3516
+ // text/plain, and copying the URL of that handed somebody a link
3517
+ // to a magnet instead of a magnet โ€” which a torrent client cannot
3518
+ // open.
3519
+ const copyable = (url, text, fetched) =>
3520
+ url
3521
+ ? `<button class="copy" data-copy-url="${escapeHtml(url)}"${
3522
+ fetched ? ' data-copy-fetch="1"' : ''
3523
+ } title="${escapeHtml(url)}">${text}</button>`
3524
+ : '';
3488
3525
 
3489
3526
  return `
3490
3527
  <div class="panel" style="margin-bottom:0.8rem">
3491
3528
  <h2 style="margin-top:0">
3492
3529
  ${escapeHtml(entry.category)}
3493
- <span class="sub">ยท ${entry.archives} archive${entry.archives === 1 ? '' : 's'}</span>
3530
+ <span class="sub">ยท ${entry.archives} build${entry.archives === 1 ? '' : 's'}</span>
3494
3531
  </h2>
3495
3532
  <div class="sub" style="margin-bottom:0.6rem">
3496
3533
  ${
@@ -3499,60 +3536,44 @@ Every piece is hashed against the ` +
3499
3536
  : 'nothing published under this tag yet'
3500
3537
  }
3501
3538
  </div>
3502
- <table>
3503
- <tbody>
3504
- ${rows
3505
- .map(
3506
- ([label, url], row) => `
3507
- <tr>
3508
- <td style="width:8rem">${label}</td>
3509
- <td>${
3510
- url
3511
- ? `<code>${escapeHtml(
3512
- url.length > 96 ? `${url.slice(0, 96)}โ€ฆ` : url,
3513
- )}</code>${
3514
- label === 'For a style'
3515
- ? `<div class="sub">the .torrent URL and the magnet ride in the
3516
- fragment, which is never sent to the server โ€” an ordinary
3517
- client fetches the TileJSON and ignores them, a swarm-aware
3518
- one reads them before making any call${
3519
- // Matched unanchored because the magnet is
3520
- // percent-encoded in the fragment now that it
3521
- // shares one with the .torrent URL.
3522
- url.includes('btpk')
3523
- ? ', and both follow the category rather than this build'
3524
- : ''
3525
- }</div>`
3526
- : ''
3527
- }`
3528
- : '<span class="sub">not a PMTiles archive, so there is no tile endpoint</span>'
3529
- }</td>
3530
- <td style="width:11rem">${
3531
- url
3532
- ? `<button data-copy="${index}-${row}">Copy</button>
3533
- <a href="${escapeHtml(url)}" target="_blank" rel="noreferrer"><button type="button">Open</button></a>`
3534
- : ''
3535
- }</td>
3536
- </tr>`,
3537
- )
3538
- .join('')}
3539
- </tbody>
3540
- </table>
3539
+ ${
3540
+ ends.tileJson
3541
+ ? `<div class="links">
3542
+ ${copyable(ends.tileJson, 'TileJSON')}
3543
+ ${link(ends.preview, 'preview')}
3544
+ ${link(ends.torrent, '.torrent', true)}
3545
+ ${copyable(ends.magnet, 'magnet', true)}
3546
+ ${copyable(ends.feed, 'RSS')}
3547
+ ${copyable(ends.latestFeed, 'RSS, newest only')}
3548
+ ${copyable(ends.styleUrl, 'style URL')}
3549
+ </div>
3550
+ <div class="sub" style="margin-top:0.5rem">
3551
+ The style URL is the TileJSON with the .torrent URL and
3552
+ the magnet in its fragment, which is never sent to the
3553
+ server โ€” an ordinary client fetches the TileJSON and
3554
+ ignores them, a swarm-aware one reads them first, and
3555
+ both follow the category rather than this build.
3556
+ </div>`
3557
+ : '<div class="sub">not a PMTiles archive, so there is no tile endpoint</div>'
3558
+ }
3541
3559
  </div>`;
3542
3560
  })
3543
3561
  .join('');
3544
3562
 
3545
- for (const button of list.querySelectorAll('[data-copy]')) {
3546
- const [index, row] = button.dataset.copy.split('-').map(Number);
3547
- const url = [
3548
- categories[index].endpoints.tileJson,
3549
- categories[index].endpoints.styleUrl,
3550
- categories[index].endpoints.torrent,
3551
- categories[index].endpoints.magnet,
3552
- categories[index].endpoints.feed,
3553
- categories[index].endpoints.latestFeed,
3554
- ][row];
3555
- button.onclick = () => copy(url, 'URL');
3563
+ for (const button of list.querySelectorAll('[data-copy-url]')) {
3564
+ button.onclick = async () => {
3565
+ const url = button.dataset.copyUrl;
3566
+ if (!button.dataset.copyFetch) return copy(url, 'URL');
3567
+ try {
3568
+ // A plain fetch, not `api`: that one parses JSON, and this
3569
+ // endpoint answers text/plain.
3570
+ const response = await fetch(url);
3571
+ if (!response.ok) throw new Error(response.statusText);
3572
+ copy((await response.text()).trim(), 'magnet');
3573
+ } catch (error) {
3574
+ toast(error.message);
3575
+ }
3576
+ };
3556
3577
  }
3557
3578
  }
3558
3579
 
@@ -408,7 +408,11 @@
408
408
  // preview is a page to visit. The rest are addresses that belong
409
409
  // somewhere else โ€” a style, a torrent client, a feed reader โ€” and
410
410
  // following one here achieves nothing, so those copy.
411
- const copy = (href, text) => {
411
+ // `fetched` for an endpoint whose *answer* is the thing worth
412
+ // having rather than its address. /magnet serves a magnet URI as
413
+ // text/plain, and copying the URL of that handed somebody a link to
414
+ // a magnet instead of a magnet โ€” which a torrent client cannot open.
415
+ const copy = (href, text, fetched) => {
412
416
  const button = el('button', 'copy', text);
413
417
  button.type = 'button';
414
418
  const absolute = new URL(href, location.href).href;
@@ -418,7 +422,10 @@
418
422
  button.title = absolute;
419
423
  button.onclick = async () => {
420
424
  try {
421
- await navigator.clipboard.writeText(absolute);
425
+ const value = fetched
426
+ ? (await (await fetch(absolute)).text()).trim()
427
+ : absolute;
428
+ await navigator.clipboard.writeText(value);
422
429
  button.classList.add('done');
423
430
  button.textContent = 'copied';
424
431
  setTimeout(() => {
@@ -444,7 +451,7 @@
444
451
  a.download = '';
445
452
  links.append(a);
446
453
  }
447
- if (ends.magnet) copy(ends.magnet, 'magnet');
454
+ if (ends.magnet) copy(ends.magnet, 'magnet', true);
448
455
  if (ends.feed) copy(ends.feed, 'RSS');
449
456
  // The one worth copying and the one nobody wants to read: a TileJSON
450
457
  // URL carrying the .torrent URL and a percent-encoded magnet in its