v3: reworked posting — three panels, drafts, duplicate detection, upload progress
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s

Posting a dump was a single form where the important choices were the easiest
to miss. It is now three panels: link or file, why & where, playlists.

Composition:
- No more URL/File toggle. An empty panel offers both ways in at once and the
  kind follows what you actually did; a file dropped anywhere in the modal is
  accepted, not just on the zone.
- Categories and visibility get their own panel instead of a disclosure that
  read as optional, and the primary button stays "Next" until they've been
  seen. Visibility carries a real label now.
- The draft (link, title, why, categories, visibility) is mirrored to
  localStorage on every change and restored on reopen, so Escape or a stray
  backdrop click costs nothing. Only an attached file can't be restored, so
  that is the one case that asks before closing.
- URL dumps can carry a poster-supplied title instead of being stuck with
  whatever the page scraped, editable right under the preview.
- Multipart uploads go through XHR so there is a real progress bar and a
  percentage on the button, rather than 50 MB of silence.

Duplicates:
- New dumps.url_canonical column (+ index, backfilled by 0013) holding a lossy
  key that ignores scheme, www., trailing slashes, tracking parameters and
  YouTube share shapes. GET /api/dumps/by-url reads it, and the create form
  warns "already dumped by X" while it fetches the preview. Never blocking.

Fixes:
- /api/preview now reports whether the page was actually reached: a failed
  fetch still yields a hostname-only stub, so a dead link and a page without
  metadata used to render identically.
- The Web Share Target never worked. The manifest posts to "/", but the index
  redirect dropped the query string, so every Android share landed on the feed
  with nothing pre-filled.
- File dumps no longer take the extension into their title.
- The link field no longer autofocuses on touch, where it raised a keyboard
  over the modal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiAPtJZeCYYk8rKehtLUQU
This commit is contained in:
khannurien
2026-09-08 14:40:37 +00:00
parent d76154d15d
commit fb8364e24d
21 changed files with 1688 additions and 304 deletions

View File

@@ -438,6 +438,222 @@
font-weight: 700;
}
/* ── Create-dump modal ── */
/* A file dropped anywhere in the modal is accepted, so the whole body lights up. */
.dump-create--drag {
outline: 2px dashed var(--color-accent);
outline-offset: 6px;
border-radius: 10px;
}
.dump-create-draft,
.dump-create-confirm {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 0.9rem;
padding: 0.55rem 0.75rem;
border-radius: 8px;
font-size: 0.85rem;
}
.dump-create-draft {
background: color-mix(in srgb, var(--color-accent) 10%, transparent);
color: var(--color-text-secondary);
}
.dump-create-draft-discard {
background: none;
border: none;
padding: 0;
cursor: pointer;
font: inherit;
font-weight: 600;
color: var(--color-link);
text-decoration: underline;
}
.dump-create-confirm {
background: var(--color-danger-bg);
border: 1px solid var(--color-danger);
}
.dump-create-confirm p {
margin: 0;
flex: 1 1 14rem;
}
.dump-create-confirm-actions {
display: flex;
gap: 0.5rem;
}
/* Not reached, or reached with nothing worth showing. */
.preview-note {
margin: 0;
font-size: 0.82rem;
color: var(--color-text-secondary);
}
/* ── "Already dumped" hint ── */
.dump-duplicate {
display: flex;
gap: 0.6rem;
padding: 0.65rem 0.8rem;
border-radius: 9px;
background: color-mix(in srgb, var(--color-accent) 9%, transparent);
border: 1px solid color-mix(in srgb, var(--color-accent) 35%, transparent);
}
.dump-duplicate-icon {
font-size: 1rem;
line-height: 1.3;
opacity: 0.8;
}
.dump-duplicate-body {
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.dump-duplicate-lead {
margin: 0;
font-size: 0.85rem;
font-weight: 600;
}
.dump-duplicate-link {
font-size: 0.85rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dump-duplicate-more,
.dump-duplicate-hint {
margin: 0;
font-size: 0.78rem;
color: var(--color-text-secondary);
}
/* ── Step indicator ── */
.dump-steps {
display: flex;
align-items: center;
gap: 0.5rem;
list-style: none;
margin: 0 0 1.1rem;
padding: 0;
}
.dump-step {
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
font-size: 0.82rem;
color: var(--color-text-muted);
}
/* Connector between steps — drawn on the gap, not on the labels. */
.dump-step + .dump-step::before {
content: "";
width: 1.1rem;
height: 1px;
background: var(--color-border-subtle);
flex-shrink: 0;
}
.dump-step-marker {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.35rem;
height: 1.35rem;
flex-shrink: 0;
border-radius: 999px;
border: 1.5px solid var(--color-border-subtle);
font-size: 0.72rem;
font-weight: 700;
line-height: 1;
}
.dump-step-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dump-step--current {
color: var(--color-text);
font-weight: 600;
}
.dump-step--current .dump-step-marker {
background: var(--color-accent);
border-color: var(--color-accent);
color: var(--color-on-accent);
}
.dump-step--done {
color: var(--color-text-secondary);
}
.dump-step--done .dump-step-marker {
border-color: var(--color-accent);
color: var(--color-accent);
}
/* Only the current step keeps its label once the modal gets narrow. */
@media (max-width: 30rem) {
.dump-step:not(.dump-step--current) .dump-step-label {
display: none;
}
}
/* ── Panels ── */
.dump-panel {
display: flex;
flex-direction: column;
gap: 1rem; /* matches .form, so panels and plain forms space identically */
animation: dump-panel-in 0.18s ease-out;
}
.dump-panel--back {
animation-name: dump-panel-in-back;
}
@keyframes dump-panel-in {
from { opacity: 0; transform: translateX(12px); }
to { opacity: 1; transform: none; }
}
@keyframes dump-panel-in-back {
from { opacity: 0; transform: translateX(-12px); }
to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
.dump-panel {
animation: none;
}
}
/* One line naming the thing the second panel's choices apply to. */
.dump-panel-recap {
margin: 0;
font-size: 0.82rem;
color: var(--color-text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ── Mode toggle — segmented control ── */
.visibility-toggle {
display: flex;
@@ -592,6 +808,41 @@
background: var(--color-border-subtle);
}
/* ── Upload progress (second panel, while the file is being sent) ── */
.dump-upload-progress {
height: 4px;
border-radius: 999px;
background: var(--color-border-subtle);
overflow: hidden;
}
.dump-upload-progress-fill {
height: 100%;
width: 0;
border-radius: 999px;
background: var(--color-accent);
transition: width 0.2s ease-out;
}
/* Length unknown, or the body is sent and the server is still thinking. */
.dump-upload-progress--indeterminate .dump-upload-progress-fill {
width: 40%;
animation: dump-upload-slide 1.1s ease-in-out infinite;
}
@keyframes dump-upload-slide {
0% { transform: translateX(-100%); }
100% { transform: translateX(250%); }
}
@media (prefers-reduced-motion: reduce) {
.dump-upload-progress--indeterminate .dump-upload-progress-fill {
width: 100%;
animation: none;
opacity: 0.5;
}
}
/* ── Local file / URL preview (DumpCreate) ── */
.local-preview-image {
width: 100%;