Files
readest/apps/readest-app/extensions/send-to-readest/popup.html
T
Huang Xin f6dfd09d82 feat(send): browser extension that clips pages into Readest as EPUBs (#4266)
Replaces the URL-only placeholder extension with a full MV3 page-clipper
that builds a self-contained EPUB on the user's machine and uploads it
to the inbox.

- Captures the rendered DOM in a content script, then runs Readability,
  asset bundling, and EPUB build through the shared
  `convertToEpub({kind: 'page'})` pipeline inside a Chrome offscreen
  document (the SW lacks DOMParser).
- Uploads the resulting EPUB directly from the offscreen page to the
  new `POST /api/send/inbox/file` endpoint — keeps the bytes in one
  realm because `runtime.sendMessage` JSON-serialises ArrayBuffer to
  `{}` between extension contexts.
- Adds a long-lived Port + ping handshake between SW, offscreen, and
  the on-demand capture content script so neither idle-eviction nor
  load-order races can hang the popup.
- Localised popup, badge feedback, key-as-content i18n (`_('English source')`)
  with an extract script that seeds locale stubs from i18n-langs.json
  and writes a static-imports map for the runtime. All 33 locales
  fully translated.
- Server: `pages/api/send/inbox/file.ts` accepts a raw EPUB body
  (Content-Type: application/epub+zip), enforces the inbox pending cap,
  stores to the existing send-inbox R2 bucket as `kind='file'`.
  `assetBundler` now sets `credentials: 'include'` in the non-Tauri
  branch so the extension SW carries paywalled-CDN cookies.
- 47 vitest cases for the extension shell (upload, badge, auth, lazy,
  popup state machine, auth-bridge token sync) + 8 cases for the new
  server endpoint. CI's `test_web_app` invokes both via the extended
  `test:pr:web` plus a `build-browser-ext` step that catches webpack
  alias / Tauri-stub regressions.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:45:29 +02:00

206 lines
5.0 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Send to Readest</title>
<style>
:root {
color-scheme: light dark;
--fg: #1a1a1a;
--muted: #666;
--surface: #ffffff;
--raised: #f5f5f5;
--border: #e5e5e5;
--accent: #1a1a1a;
--accent-fg: #ffffff;
--ok: #1e8e3e;
--err: #c0392b;
}
@media (prefers-color-scheme: dark) {
:root {
--fg: #f0f0f0;
--muted: #a8a8a8;
--surface: #1a1a1a;
--raised: #262626;
--border: #333;
--accent: #f0f0f0;
--accent-fg: #1a1a1a;
--ok: #4ade80;
--err: #f87171;
}
}
html,
body {
margin: 0;
padding: 0;
background: var(--surface);
color: var(--fg);
}
body {
width: 320px;
padding: 16px;
font:
13px/1.45 system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
h1 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.signed-out-badge {
font-size: 11px;
padding: 2px 6px;
border-radius: 999px;
background: var(--raised);
color: var(--muted);
}
.page {
background: var(--raised);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 12px;
}
.page-title {
font-size: 13px;
font-weight: 600;
margin: 0 0 4px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.page-url {
font-size: 11px;
color: var(--muted);
word-break: break-all;
margin: 0;
}
button.primary {
width: 100%;
padding: 9px 12px;
font-size: 13px;
font-weight: 600;
color: var(--accent-fg);
background: var(--accent);
border: 0;
border-radius: 8px;
cursor: pointer;
}
button.primary:disabled {
opacity: 0.5;
cursor: default;
}
button.link {
background: transparent;
border: 0;
padding: 0;
color: var(--fg);
font: inherit;
cursor: pointer;
text-decoration: underline;
}
.progress {
margin-top: 10px;
display: none;
}
.progress.show {
display: block;
}
.progress-label {
font-size: 12px;
color: var(--muted);
margin-bottom: 4px;
}
.progress-bar {
height: 4px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
width: 0;
background: var(--accent);
transition: width 200ms ease;
}
.progress-bar.indeterminate .progress-bar-fill {
width: 30%;
animation: scan 1.1s ease-in-out infinite;
}
@keyframes scan {
0% {
margin-left: -30%;
}
100% {
margin-left: 100%;
}
}
.status {
font-size: 12px;
}
/* Only reserve vertical space once there's actual text — an empty
status used to leave a 16 px gap below the progress bar that made
the bottom padding look bigger than the top. */
.status:not(:empty) {
margin-top: 10px;
}
.status.ok {
color: var(--ok);
}
.status.err {
color: var(--err);
}
[hidden],
.hidden {
display: none;
}
.sign-in {
text-align: center;
padding: 20px 0 4px;
}
.sign-in p {
margin: 0 0 12px;
color: var(--muted);
}
</style>
</head>
<body>
<header>
<h1 data-i18n="Send to Readest"></h1>
<span id="auth-badge" class="signed-out-badge hidden" data-i18n="Signed out"></span>
</header>
<section id="signed-in-view">
<div class="page">
<p id="page-title" class="page-title" data-i18n="Loading…"></p>
<p id="page-url" class="page-url"></p>
</div>
<button id="send" class="primary" disabled data-i18n="Send to Readest"></button>
<div id="progress" class="progress">
<div class="progress-label" id="progress-label"></div>
<div class="progress-bar indeterminate"><div class="progress-bar-fill"></div></div>
</div>
<p id="status" class="status"></p>
</section>
<section id="signed-out-view" class="sign-in hidden">
<p data-i18n="Sign in to Readest to start clipping pages."></p>
<button id="open-readest" class="primary" data-i18n="Open web.readest.com"></button>
</section>
<script src="popup.js" defer></script>
</body>
</html>