From a02c562a9fda2997922f960c1476332e16680973 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 19 May 2026 14:37:36 -0400 Subject: [PATCH] Custom Art metadata simplification, JPEG mask fix, README rewrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Custom Art project metadata: - Hide the "Add Media Identifiers" panel when in Custom Art mode — IMDb / UPC / TheTVDB / MusicBrainz don't fit wallpaper / banner / logo work. - Show a single optional Custom tag field in its place. Stored as state.meta.customTag, round-trips through save/load, and emits to Nostr as both an NIP-73-style 'i custom:' identifier and a topical 't ' tag (lowercased, hyphenated, capped at 64 chars). - Hide the IMDb import button + hint in Custom Art mode. - Auto-select the "Custom art" category the first time the user enters customart mode without one set. JPEG export mask fix: - body.export-print > *:not(#exportPrintContainer) { display: none } was hiding the global SVG container that holds definitions for layer masks. With the defs invisible, every clip-path: url(#…) reference resolved to nothing during print, so the PDF that the JPEG flow re-rasterizes had unclipped layers. Carve #globalClipDefs out of the exclusion selector and force it to zero-size + overflow:hidden so it stays in the DOM but takes no space. Direct "Print / Save PDF" was unaffected because it doesn't use the export-print mode. README rewrite: - Brings the README in line with what the app actually does today: Custom Art mode, vector shape primitives + gradients + strokes + custom SVG upload with per-element editor, clipping/masking, layer clipboard, profile pages at /u/:npub, Lightning tips with WebLN auto-pay + NIP-57 zap tracking + Zapped tab, edit-in-place via NIP-33 replaceable semantics, the publish-confirm modal, the current schema version (5 / min 4), and the vendored qrcode lib. Remove docs/VECTOR_SHAPES_FEATURE.md — the feature shipped. Co-Authored-By: Claude Opus 4.7 --- README.md | 128 +++++++---- docs/VECTOR_SHAPES_FEATURE.md | 395 ---------------------------------- src/index.html | 54 ++++- 3 files changed, 134 insertions(+), 443 deletions(-) delete mode 100644 docs/VECTOR_SHAPES_FEATURE.md diff --git a/README.md b/README.md index b86e6b1..6e4ba18 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,104 @@ # CaseWrap Studio -CaseWrap Studio is a static print-layout editor for physical media packaging. -It supports DVD/Blu-ray case wraps, Avery 8960/8944 disc-label sheets, Avery 8693/8943 CD jewel-case insert sheets, and reusable single-disc designs. +CaseWrap Studio is a static, browser-only design tool for printable physical media packaging — and increasingly, a general-purpose Nostr-native graphics editor. It started as a DVD/Blu-ray case wrap maker and now also covers Avery 8960/8944 disc labels, Avery 8693/8943 CD jewel-case inserts, single-disc designs, and free-form custom art canvases. Designs publish to and load from Nostr, are forkable and editable in place, and can be tipped via Lightning. -## What it supports +## Modes -- Separate front, spine, and back case artwork (or one combined wrap image) -- Disc-label sheet mode (two print positions on one Avery 8960/8944 sheet) -- CD jewel insert mode (front + tray inserts on one Avery 8693/8943 sheet) -- Disc Designer mode for creating a reusable single-disc design -- Additional image and text layers -- Drag positioning, z-order changes, rotation, sizing, and opacity -- PDF export through the browser print dialog -- Nostr community browsing/sharing for templates and disc designs -- Publishing, replies, reactions, follows, and reposts from inside the app +The editor has two tools at the top level: -## Important workflow +- **Template** — designs that map to a real printed surface. + - **Case cover** — DVD / Blu-ray wraps with separate front / spine / back artwork or a single combined wrap image. + - **Disc labels** — Avery 8960 / 8944 sheets with two disc positions. + - **CD jewel inserts** — Avery 8693 / 8943 sheets with a front insert + tray insert. + - **Custom art** — free-form canvas with size presets (1920×1080, square sizes, etc.) or custom dimensions. Treats the project as generic art rather than packaging. +- **Disc Designer** — make a reusable single-disc design, publish it on Nostr, and import it later into either disc position of a disc-label sheet. -All image sources are remote URLs. +Each mode keeps its own metadata (title / identifiers / category / language) so loading a template in the Disc Designer doesn't overwrite the Designer's work in progress, and vice versa. -That means: +## Layers -- Case art, disc art, jewel insert art, and extra image layers should use `http` or `https` URLs -- Saved project JSON remains portable across machines -- Exported/copied Nostr events can reconstruct layouts from event content -- Community feed data comes from relays listed in the Community panel -- Publishing/social actions require a NIP-07 browser extension (for example Alby or nos2x) +Layers compose on top of the per-mode artwork: -If a URL stops working later, the image will no longer render. For long-term reliability, use stable hosted URLs. +- **Image** — remote URL, with object-fit / object-position controls. +- **Text** — rich editable text with font-family picker (web-safe families), size, color, bold/italic, alignment. +- **Color block** — solid-color rectangle for tints and backgrounds. +- **Shape** — vector primitives (rectangle / rounded rectangle / circle / ellipse / triangle / polygon / star / line) with solid, linear-gradient, or radial-gradient fills and optional solid strokes (color / width / solid / dashed / dotted). +- **Custom path / SVG upload** — upload an `.svg` file. Single-path SVGs land as editable shapes; multi-element SVGs preserve their internal fills and stacking, with a per-element editor that lets you change each `` / `` / `` etc. fill and stroke individually. -## Main file +Every layer supports drag / resize / rotate / opacity / z-order, an aspect-ratio lock toggle on the W/H inputs (default on), and a lock toggle that prevents accidental drags. -- `src/index.html` is the complete app entrypoint. Open it directly in a browser or serve it from any static host. +### Layer clipping / masking -## Export and print +Any layer (image, text, color, shape) can be clipped to a shape: -- Use `Print / Save PDF` for print-ready output -- Keep browser scale at `100%` -- Disable browser headers and footers +- **Inline shape clip** — pick a primitive (rect / circle / star / etc.) to mask the layer to that shape. +- **From another layer** — point at another shape layer in the same target and use its geometry as the mask. -## Save, restore, and sharing +Renders consistently in the editor, the preview surfaces, and the canvas / PDF / JPEG export. -- `Save project` writes project JSON -- `Load project` restores project JSON -- `Copy Nostr event` copies a signed event payload -- `Import from Nostr` restores a template/design from a copied event -- The Community modal lets you browse New/Popular/Search/Mine, preview templates, and import them -- Disc designs can be published and reused independently, then imported into Disc 1 or Disc 2 positions on a disc sheet +### Layer clipboard -## Nostr data model +Cross-target and cross-tool copy/paste — Copy / Cut on a selected layer, Copy all / Paste for the whole panel. Paste retargets to the current sub-mode and clamps positions so layers copied from a wide cover don't land off-canvas on a narrow disc. -- Template/design payloads are published as kind `30078` app-data events with `casewrap` tags -- Typical social events use standard kinds (`1`, `7`, `6`, `3`, `0`) -- Template modes include `cover`, `disc`, `jewel`, and `disc-design` -- Current schema version is `5`; minimum supported schema version is `4` +## Nostr integration -## Notes +Every design surface participates in the same Nostr-backed community feed. -- The app is fully static and does not require a backend -- Local browser autosave is still available -- Older saved projects with embedded data URLs may still load, but new work should use remote URLs +- **Identity** via NIP-07 (Alby, nos2x, etc.). +- **Templates and designs** publish as kind-30078 parameterized replaceable events tagged `casewrap-`, with the `d`-tag mode-prefixed so cover/disc/jewel/custom-art/disc-design designs for the same title don't collide. +- **Community modal** browses New / Popular / Zapped / Search / My Designs with category and language filters, plus optional WoT trust filters when signed in. +- **Profile pages** at `/u/` show the author's banner, avatar, NIP-05, npub (copyable), about, Lightning address, and the grid of their published designs. Profile state is cached per session. +- **Replies** (kind-1), **reactions** (kind-7), **reposts** (kind-6), and **follows** (kind-3) all work from inside the app. +- **Soft moderation** — kind-10000 mute lists and kind-1984 reports from your hop-1 follows blur cards by default, with a one-click reveal. +- **Block list** — local first, syncs to your kind-10000 on the next login. +- **Edit published designs** — owners can republish a kind-30078 with the same d-tag to supersede the original on relays via NIP-33 semantics. The publish-confirm modal explains the trade-off (reactions and Lightning zaps on the old version stay on relays but orphan to the new event id). +- **Fork / remix** — every published design has a Fork button that loads it into the editor as a starting point under your own pubkey. +- **Share links** — `/share/` or `/share/` URLs resolve cold. + +## Lightning + +Two flavors of Lightning are wired in: + +- **Tips** — pure LNURL-pay, no NWC required. Pick an amount (21 / 100 / 500 / 1k / 5k / 21k / Custom), the recipient's `lud16` or `lud06` resolves to an LNURL-pay endpoint, and you get a bolt11 invoice + QR + lightning: deep-link. If the recipient's wallet supports zap requests (`allowsNostr: true`), we also sign a NIP-57 kind-9734 zap request and pass it along, so a kind-9735 receipt lands on relays after settlement. +- **Tracked zaps** — kind-9735 receipts feed a per-event aggregate, surfaced as `⚡ ` on feed cards / previews / profile pages, and as a sortable **Zapped** tab in the Community modal. +- **WebLN auto-pay** is offered as a one-click button when an extension is detected. +- **Payment detection** — after a tracked invoice is issued, a poll watches for the receipt and auto-closes the tip modal when payment is confirmed. + +## Save, share, restore + +- **Save project** writes the design (with metadata) to a JSON file. +- **Load from file** detects whether the JSON is a template or a disc design and switches the active tool automatically. +- **Publish to Nostr** opens a preview/metadata confirmation modal before signing. +- **Copy share link** copies an `naddr1…` (replaceable-event address) or `nevent1…` (specific event id) URL. +- **Copy event ID** is available once a project has been published. +- **Export JPEG** — round-trips through the browser's PDF export and PDF.js to produce a flattened image. Useful when you want a single shareable asset. + +## Print + +- `Print / Save PDF` produces print-ready output. Keep browser zoom at 100% and disable headers/footers for predictable margins. +- Print-time toggles (`Show bleed`, `Show crops`, `Show guides`) control which guides are inked. + +## Workflow notes + +- All image references are **remote URLs**. Paste an `http`/`https` image URL into any slot or layer — the app never embeds binary image data into a project (keeps payloads small and Nostr-friendly). +- If a URL goes 404 later, the design still loads — the image just doesn't render. Pick stable hosts. We persist a broken-image set to `localStorage` so once we've detected a 404, it stays sunk to the bottom of ranked feeds across reloads. +- Saved project JSON is portable. The same JSON is what publishes to Nostr (after stripping local-only fields). + +## Files + +- `src/index.html` — the complete single-file app. Open it directly in a browser, or serve it from any static host. Vercel rewrites in `vercel.json` map `/share/:id` and `/u/:npub` to the SPA entry. +- `src/vendor/qrcode.min.js` — vendored QR encoder (used for Lightning tip invoices). +- `docs/` — feature plans for unbuilt work (badges, collections, QR layer, TMDB autofill, undo/redo, NWC zaps, zap-gated templates). + +## Schema + +- Current project schema version: **5**. +- Minimum supported version: **4**. Older payloads load but won't round-trip cleanly. +- Template-mode discriminator in payloads: `cover`, `disc`, `jewel`, `customart`, `disc-design`. +- Layer types: `image`, `text`, `color`, `shape`. Shape kinds: `rect`, `rounded-rect`, `circle`, `ellipse`, `triangle`, `polygon`, `star`, `line`, `path`, `svg`. Any layer may carry a `clip` field for masking. + +## Defaults and stack + +- No backend. Fully static. No build step. +- NIP-07 browser extension required for any signing-side action (publish, reply, react, follow, block-sync, zap request, edit, delete). +- Lightning is **optional**. Without a signed-in NIP-07 wallet, tips fall back to plain LNURL-pay and aren't tracked as zaps. diff --git a/docs/VECTOR_SHAPES_FEATURE.md b/docs/VECTOR_SHAPES_FEATURE.md deleted file mode 100644 index 37a6ff2..0000000 --- a/docs/VECTOR_SHAPES_FEATURE.md +++ /dev/null @@ -1,395 +0,0 @@ -# Vector Shapes, Fills, and Masking - -## Goals -Expand the layer system from "image / text / color rectangle" into a real **vector design surface**: shape primitives, custom paths, solid + gradient fills, strokes, and clip/mask composition. All purely client-side, all JSON-serializable, all backwards-compatible with existing templates and the existing print/PDF/JPEG export. - -### Product goals -1. Open the door to creative work that requires more than photos + text — accents, frames, geometric overlays, stylized backgrounds, branded shapes. -2. Make masking (circle crops, star-shaped photos, gradient-faded artwork) possible inside the editor without external tools. -3. Stay print-safe: vectors render crisply at any output resolution. -4. Round-trip through the existing project JSON and Nostr publish flow with no migration step. - -### Non-goals (v1) -- A pen tool / interactive bezier editing surface. Users paste an SVG `d=` string for custom paths. -- SVG import as a whole (parsing `` files, handling nested groups). Single `` only. -- Conic gradients, mesh gradients, patterns, image-fill of shapes. -- Outside / inside stroke positions (center stroke only — outside-positioned strokes require path inflation). -- Stroke dash editing UI. v1 picks from a small preset list. -- Mask groups (multiple shapes union'd as a single mask). v1 supports a single shape clip per layer or a single layer-as-mask reference. -- Animated gradients or shapes. - ---- - -## Current state - -Layers today are objects in `state.layers[]`: - -```js -{ id, type, name, target, x, y, w, h, rotate, opacity, z, locked, …type-specific } -``` - -with three concrete types: -- `image` → `src`, `fit?` -- `text` → `html`, `fontSize`, `color`, `align`, `bold`, `italic`, `fontFamily` -- `color` → `color` (flat rectangle fill, currently the closest we have to a shape primitive) - -Renderers branch on `type` in roughly five places: -1. `renderLayers` (main editor canvas) -2. `renderTemplatePreviewDOM` (cover preview) -3. `renderDiscSheetPreviewDOM` (disc-sheet preview) -4. `renderJewelPreviewDOM` (jewel preview) -5. `renderDiscPreviewDOM` (disc-design preview) -6. `renderCustomArtPreviewDOM` (custom art preview) -7. Canvas rendering for PDF/JPEG export - -Each renderer has the same shape: -```js -if (layer.type === 'text') { … } -else if (layer.type === 'image' && layer.src) { … } -else if (layer.type === 'color') { … } -``` - -Coordinates are in **inches** for `x`, `y`, `w`, `h`. - ---- - -## Data model - -### New layer type: `shape` - -A single mega-type covers every primitive plus custom paths, with a discriminated `shape.kind` field. This keeps the number of renderer branches at one (not seven, one per primitive). - -```js -{ - id: 'layer-…', - type: 'shape', - name: 'Star burst', - target: 'cover-trim' | 'back' | 'spine' | 'front' | 'disc-top' | 'disc-bottom' | 'jewel-front' | 'jewel-tray' | 'designer-disc' | 'canvas', - x, y, w, h, rotate, opacity, z, locked, - shape: { - kind: 'rect' | 'rounded-rect' | 'ellipse' | 'circle' | 'polygon' | 'star' | 'line' | 'path', - // kind-specific (all optional except where noted): - cornerRadius?: number, // rounded-rect (in shape-local units, 0..min(w,h)/2) - sides?: number, // polygon (default 6) - points?: number, // star (default 5) - innerRadiusRatio?: number, // star (0..1, default 0.45) - d?: string, // path — SVG path data normalized to a 0..1 viewbox - x1?, y1?, x2?, y2?: number // line endpoints (0..1 in shape-local) - }, - fill: { - type: 'none' | 'solid' | 'linear-gradient' | 'radial-gradient', - color?: '#hex', // solid - stops?: [{ at: 0..1, color: '#hex' }, …], // gradients, at least 2 stops - angle?: number, // linear gradient, degrees - center?: { x: 0..1, y: 0..1 },// radial gradient center (in shape-local coords) - radius?: number // radial gradient radius (0..1) - }, - stroke: { - type: 'none' | 'solid', - color?: '#hex', - width?: number, // inches - dash?: 'solid' | 'dashed' | 'dotted' | string // string = SVG dash array - }, - clip?: { - kind: 'inline-shape', - shape: { /* full shape sub-object, same structure as `shape` above */ } - } | { - kind: 'layer-ref', - layerId: 'layer-…' - } -} -``` - -The `clip` field is **optional and can be applied to any layer type**, not just shapes. So an `image` layer can have `clip: { kind: 'inline-shape', shape: { kind: 'circle' } }` to mask a photo into a circle without needing a separate shape layer. - -### Why one mega-type instead of one per primitive - -- Keeps renderer code to a single `case 'shape':` branch in every renderer. -- Selected-layer controls dispatch on `shape.kind` for kind-specific params (corner radius, sides, etc.) but share the fill / stroke / clip panels. -- Easier to add a new `kind` later (e.g. `arrow`, `speech-bubble`) without touching every renderer. - -### Why a discriminated `clip` object - -- `kind: 'inline-shape'` covers 90% of users — "crop my photo to a circle / star / heart." -- `kind: 'layer-ref'` covers the power case — "let me use *that* hand-drawn vector shape as a mask for *this* image." -- A future `kind: 'union'` could combine multiple mask shapes; not v1. - -### Backwards / forwards compat - -- Existing `color` layers are untouched. A flat-color rectangle is now possible two ways: legacy `type: 'color'` (kept forever) and new `type: 'shape', shape: { kind: 'rect' }, fill: { type: 'solid' }`. The renderer handles both. -- Old clients seeing `type: 'shape'` skip the layer — the rest of the design still renders. Same fail-soft behavior as today's old clients ignoring `type: 'color'`. -- Future `shape.kind` values we haven't shipped yet are also skipped individually by clients that don't recognize them. -- `clip` on existing layer types is purely additive. Clients that don't know about it render the layer un-clipped, which is uglier but not broken. - -### JSON example (one shape layer with a gradient and a clip) - -```json -{ - "id": "layer-1742000000-001", - "type": "shape", - "name": "Accent burst", - "target": "cover-trim", - "x": 1.5, "y": 0.5, "w": 1.0, "h": 1.0, - "rotate": 0, "opacity": 0.85, "z": 7, - "shape": { - "kind": "star", - "points": 8, - "innerRadiusRatio": 0.4 - }, - "fill": { - "type": "linear-gradient", - "angle": 135, - "stops": [ - { "at": 0, "color": "#fbbf24" }, - { "at": 1, "color": "#dc2626" } - ] - }, - "stroke": { - "type": "solid", - "color": "#7f1d1d", - "width": 0.01, - "dash": "solid" - } -} -``` - -That's it. No new top-level fields on the project JSON, no migration step, no external assets. - ---- - -## Rendering - -### DOM (editor + 6 preview surfaces) - -Today each layer renders as a `
` positioned absolutely with `transform: translate() rotate()`. For shape layers, we keep the `
` wrapper (so the position / rotation / opacity / drag-handle code is unchanged), and inject an inline `` filling 100% of the wrapper. - -The SVG sets `viewBox="0 0 100 100"` (a normalized coordinate space) so all shape math works in 0–100 units regardless of the actual layer w/h in inches. Stroke width converts: `(stroke.width / max(w_inches, h_inches)) * 100`. - -```js -function renderShape(svgEl, layer) { - const { shape, fill, stroke } = layer; - const defs = []; - - if (fill.type === 'linear-gradient' || fill.type === 'radial-gradient') { - defs.push(buildGradientDef(layer.id, fill)); - } - - const path = buildShapePath(shape); // returns SVG / / etc. - applyFill(path, fill, layer.id); - applyStroke(path, stroke, layer); - - svgEl.innerHTML = `${defs.join('')}${path}`; -} -``` - -`buildShapePath` is a switch on `shape.kind`: -- `rect`: `` (with `rx` for rounded-rect) -- `circle` / `ellipse`: `` -- `polygon`: compute `` from `sides` -- `star`: compute `` from `points` + `innerRadiusRatio` -- `line`: `` -- `path`: `` - -Inline gradients are referenced via `fill="url(#grad-)"`. The gradient `id` is namespaced by layer id so multiple shape layers don't collide. - -### Canvas (PDF / JPEG export) - -Canvas2D natively supports everything we need: -- `ctx.beginPath() / ellipse() / rect() / moveTo() / lineTo() / fill() / stroke()` -- `ctx.createLinearGradient(x0,y0,x1,y1) / addColorStop(...)` -- `ctx.createRadialGradient(...)` for radial -- `ctx.setLineDash([...])` for dash patterns -- `ctx.clip()` for masks (called after a fill or shape path is built but before the next layer draws — needs save/restore discipline) - -The canvas renderer parallels the SVG renderer 1:1. Each shape kind has a "build the path on the context" function that the SVG renderer would translate to attributes. - -### Print/PDF fidelity - -- SVG renders crisply in `window.print()` PDF in all modern browsers. We've already verified this with the existing text layer rendering. -- `clip-path: path(…)` is well-supported in Chromium (Chrome, Edge, Brave) and Safari ≥ 16. Older Firefox versions had known bugs. -- Fallback: when print fidelity matters most (the JPEG export path), we route through canvas, where `ctx.clip()` is rock-solid. - ---- - -## UX - -### Adding shapes -Next to the existing "Add image / Add text / Add color block" buttons in each layer panel: - -``` -[ + Image ] [ + Text ] [ + Color ] [ + Shape ▾ ] - │ - ├─ Rectangle - ├─ Rounded rect - ├─ Circle - ├─ Ellipse - ├─ Triangle - ├─ Polygon - ├─ Star - ├─ Line - └─ Custom path (SVG)… -``` - -Clicking a primitive inserts a centered shape at 50% of the target bounds with a sensible default fill (the existing accent blue at 100% opacity). - -"Custom path (SVG)" opens a small modal: -- Textarea for the `d=` data -- Live preview as the user pastes -- Validate: parse `d` and reject anything containing `<`, `>`, or `script` (defensive; we never `innerHTML` the raw data, only set it as a `d` attribute) -- "Add to canvas" inserts it as a path-kind shape layer - -### Selected-layer controls -When a shape layer is selected, the existing "Selected layer" panel grows three sub-panels (in addition to the existing position/rotation/opacity controls): - -1. **Shape** — kind-specific params: - - rounded-rect: corner radius slider - - polygon: sides number (3..12) - - star: points number (3..12) + inner-radius slider - - line: stroke-width is the visible attribute; the path is built from endpoints - - path: a small "Edit path data…" button that reopens the SVG textarea modal - -2. **Fill** — fill-type select (none / solid / linear / radial), with conditional sub-controls: - - solid → one `+ Add Media Identifiers
@@ -2039,6 +2049,11 @@

Optional — used for search and cross-referencing. Fields shown reflect what's common for this category.

+ @@ -2054,10 +2069,10 @@ -
+
-

IMDb import uses OMDb-compatible endpoint + API key. Title and category are required to save or export.

+

IMDb import uses OMDb-compatible endpoint + API key. Title and category are required to save or export.

@@ -3662,6 +3677,19 @@ const visible = !allowed || allowed.has(key); el.style.display = visible ? '' : 'none'; }); + // Custom Art mode collapses the standard media-identifier panel and + // the IMDb import affordance, and shows a single optional "Custom tag" + // field instead — IMDb / UPC / TheTVDB / MusicBrainz don't apply to + // generic art / wallpaper / banner work. + const isCustomArt = state.templateMode === 'customart'; + const idWrap = $('mediaIdentifiersWrap'); + const customWrap = $('customTagWrap'); + const imdbRow = $('imdbImportRow'); + const imdbHint = $('imdbImportHint'); + if (idWrap) idWrap.style.display = isCustomArt ? 'none' : ''; + if (customWrap) customWrap.style.display = isCustomArt ? '' : 'none'; + if (imdbRow) imdbRow.style.display = isCustomArt ? 'none' : ''; + if (imdbHint) imdbHint.style.display = isCustomArt ? 'none' : ''; } function categoryOptions(emptyLabel = 'Any category') { @@ -3737,6 +3765,7 @@ const upc = (payload?.meta?.upc || '').trim(); const tvdb = (payload?.meta?.tvdbId || '').trim(); const mbDisc = (payload?.meta?.musicbrainzDiscId || '').trim(); + const customTag = (payload?.meta?.customTag || '').trim(); const category = payload?.meta?.category || payload?.category || ''; const language = payload?.meta?.language || 'en'; const templateType = payload?.templateType || ''; @@ -3750,6 +3779,12 @@ if (upc) tags.push(['i', `upc:${upc}`]); if (tvdb) tags.push(['i', `tvdb:${tvdb}`]); if (mbDisc) tags.push(['i', `musicbrainz:${mbDisc}`]); + if (customTag) { + // Free-form user tag for custom art. Emitted as both an 'i' identifier + // (mirroring the other IDs) and a topical 't' tag for search filters. + tags.push(['i', `custom:${customTag}`]); + tags.push(['t', customTag.toLowerCase().replace(/\s+/g, '-').slice(0, 64)]); + } tags.push(['t', 'casewrap']); if (typeTag) tags.push(['t', typeTag]); if (templateType) { @@ -4279,6 +4314,7 @@ const upcEl = $('upcField'); if (upcEl) upcEl.value = state.meta?.upc || ''; const tvdbEl = $('tvdbField'); if (tvdbEl) tvdbEl.value = state.meta?.tvdbId || ''; const mbEl = $('musicbrainzField'); if (mbEl) mbEl.value = state.meta?.musicbrainzDiscId || ''; + const customTagEl = $('customTagField'); if (customTagEl) customTagEl.value = state.meta?.customTag || ''; const cf = $('categoryField'); if (cf) cf.value = state.meta?.category || ''; const lf = $('languageField'); if (lf) lf.value = state.meta?.language || 'en'; const dt = $('designerTitleField'); if (dt) dt.value = state.meta?.title || ""; @@ -9030,7 +9066,7 @@ } function blankMeta() { - return { title: '', imdbId: '', upc: '', tvdbId: '', musicbrainzDiscId: '', omdbApiKey: state.meta?.omdbApiKey || '', category: '', language: 'en' }; + return { title: '', imdbId: '', upc: '', tvdbId: '', musicbrainzDiscId: '', customTag: '', omdbApiKey: state.meta?.omdbApiKey || '', category: '', language: 'en' }; } function enterEditMode({ dTag, kind, eventId, title }) { @@ -13361,6 +13397,12 @@ state.preset = 'dvd'; Object.assign(state, presets.dvd); } + // Auto-pick the "Custom art" category the first time the user + // enters customart mode without one set — saves them a step and + // matches what most users would have picked anyway. + if (mode === 'customart' && !state.meta.category) { + state.meta.category = 'custom-art'; + } syncInputs(); render(); }); @@ -13427,6 +13469,8 @@ const tvdbInput = $("tvdbField"); if (tvdbInput) tvdbInput.addEventListener("input", (e)=>{state.meta.tvdbId=e.target.value; render();}); const mbInput = $("musicbrainzField"); if (mbInput) mbInput.addEventListener("input", (e)=>{state.meta.musicbrainzDiscId=e.target.value; render();}); $("categoryField").addEventListener("change", (e)=>{state.meta.category=e.target.value; syncIdentifierVisibility(); render();}); + const customTagInput = $("customTagField"); + if (customTagInput) customTagInput.addEventListener("input", (e)=>{state.meta.customTag = e.target.value; render();}); $("languageField").addEventListener("change", (e)=>{state.meta.language=e.target.value; render();}); $("designerTitleField").addEventListener("input", (e)=>{state.meta.title=e.target.value; if (state.discDesign) state.discDesign.name = e.target.value; render();}); $("designerImdbField").addEventListener("input", (e)=>{state.meta.imdbId=e.target.value; render();});