Repository navigation
Conversation
Bumps to 2.0.0-alpha.1 and brings the build config in line with the other Diamond apps. - Add installer.nsh registering a 'Convert with Diamond File Converter' Explorer entry for both files and directories, plus fileAssociations for all 21 supported extensions. File Converter was the only one of the three apps with neither, so it had no shell integration at all. - Bundle ffprobe-static alongside ffmpeg. ffmpeg-static ships only ffmpeg.exe, so no metadata was previously available anywhere in the app. - Drop fluent-ffmpeg. Pipelines need filter_complex and explicit -map, which it obscures, and -progress pipe:1 reports more reliably than its percent field did. - Rename funding.yml to FUNDING.yml. GitHub requires the uppercase name, so the sponsor button was not rendering. - Add an npm test script.
The v1 engine was a single fluent-ffmpeg call, ffmpeg(input).save(output),
with no codec, bitrate, resolution, trim, quality or stream-copy control at
any layer. These modules are the spine the 2.0 features need. They import no
Electron, so they are unit-testable without launching the app.
- formats.js Capability graph replacing the within-group cross-product.
Cross-kind conversions are now representable: video to audio,
video to frames or a thumbnail, images to video. Subsumes the
gif-to-still special case that was hardcoded in main.js.
- job.js Job schema with defaults derived from target-format
capabilities, nested-merge patching for bulk edit, and
validation that rejects impossible combinations without
requiring metadata to be present.
- ffmpeg-args.js Pure buildArgs(job) -> string[]. Stream copy, crf/bitrate,
presets, resize with contain/cover/stretch, trim via input
seek plus duration, per-format image quality, a palettegen
chain for GIF, frame patterns, faststart.
- runner.js Concurrency pool, per-job ids, per-job cancel, pause/resume,
partial-output cleanup, structured progress parsed from
-progress pipe:1. Replaces the single activeConversion global.
- probe.js ffprobe wrapper with normalised output. Treats metadata as
enrichment, so a failed probe never blocks a job. Discriminates
cover art from real video streams.
- paths.js Output routing (alongside, fixed, mirrored source tree), name
templating and conflict policies. Output was previously
hardcoded to the input directory.
- scan.js Async recursive directory walk with depth and count limits and
extension filters, replacing a synchronous one-level readdir
that ran on the UI thread and discarded nested files.
- pipeline.js Node graph schema and filter_complex compiler with cycle,
port-type and reachability validation. Schema and compiler
only; the flowchart editor is deliberately not in this slice.
53 tests cover the argument builder, the pipeline compiler, path resolution
against a real filesystem, progress parsing and the directory scanner.
…ayer The renderer ran with nodeIntegration:true and contextIsolation:false and no preload, requiring fs, path and electron directly. It now has no Node access at all; everything goes through window.electronAPI, matching the name the Dropgate client already uses. - preload.js exposes the bridge. It runs sandboxed, where require() is limited to a small allowlist, so its channel names are inlined rather than imported from constants.js. Requiring anything else there silently leaves window.electronAPI undefined and the app inert with no error in main, so test/preload.test.js guards both the allowlist and channel-name drift. - IPC channels are namespaced by domain. Progress and status now carry a jobId, which is what makes concurrency possible; v1 pushed a bare integer with no way to tell which file it belonged to. The convert result is an object rather than an overloaded string|'cancelled'|null whose string could be either a file or a directory. - The renderer no longer stats the filesystem to discover whether an output was a folder of frames; the result carries isDirectory and fileCount. - Directory expansion on drop moves to the main process. - Add a document-level drop guard. Dropping outside the zone previously navigated the window to the file, replacing the app with it. - Fix dragleave firing on child boundaries, which made the highlight flicker. - credits.html loses its require() and inline onclick handlers. - Handle file associations properly: parse argv, add an open-file handler, and forward paths from second-instance, which previously discarded them so 'Open with' on a running app did nothing. Explorer spawns one process per selected file, so arrivals are debounced. - Extend electron-store beyond windowBounds with a real settings schema. - Remove dead code: the unused get-version/get-setting/set-setting handlers, the globalShortcut import, and the .btn-accent, .bg-accent and .btn-format-disabled rules. styles/tokens.css is the first real token layer in any of these apps, and it fixes a live bug: --bs-primary was remapped to the accent only inside [data-bs-theme=dark], so the primary Convert button rendered Bootstrap blue in light mode and green in dark. It is now bound in both themes and on the bare :root, which also covers the system-default case.
docs/design-system/ holds 20 generated artboards documenting the visual language shared by Diamond Media Player, the Dropgate client and Diamond File Converter, surveyed across the three codebases. Each card is a self-contained page carrying a @DSCARD marker; build.js regenerates the set. The same bundle is published as the 'Diamond Digital - Electron House Style' design system, so the reference is not only version-controlled here. Covered: the accent and token layer, the two wash formulas, typography, spacing and radii, motion, buttons, drop zone, file row, toggle, status card, progress, toast, modal, form rows, window chrome, popup windows, the Explorer context menu, settings placement, feedback and theming. The agreed house rule is per-app accent, everything else shared. Today the apps use green, purple and stock Bootstrap blue, and none had a token layer, so the accent was applied by hand-written hexes.
The v1 UI was a single column with one shared target format per media kind and
a strictly sequential convert loop. Jobs are now independently configurable
cards that run through the runner's concurrency pool, which has been sitting
unused since the architecture slice.
- Each card carries its own source, target, metadata and status. Cards render
the moment a file is dropped and fill in duration, resolution, codec and
size when the probe resolves, so a slow probe never blocks the grid.
- Selection with click, ctrl+click, shift+click ranges, ctrl+A, Escape and
Delete. A plain click inside an existing multi-selection keeps it, so acting
on a group does not collapse it first.
- The bulk bar offers only formats every selected source can actually produce
- the intersection - so a bulk change cannot create an invalid job, and it
says so when a mixed selection has no common target.
- Per-card progress and per-card cancel, both already supported by the runner.
Unknown-duration jobs animate rather than sitting at 0%.
- Folder ingest is wired to the recursive scanner, and reports what it
skipped. v1 discarded unsupported files in silence, which read as the app
being broken.
Failures no longer open a blocking native dialog per file, which with a
concurrency pool would stack several at once and halt the batch. Each card
shows its own ffmpeg reason with a Retry action, and a single toast summarises
the run. The toast component existed only in the Dropgate server; this is the
first desktop implementation.
Error text is now selectable. body { user-select: none } is scoped to the
chrome so filenames, paths and failure messages can be copied - previously a
user could not copy an error in order to report it.
Two accent bugs fixed alongside: .progress hardcodes --bs-progress-bar-bg to
Bootstrap blue rather than deriving it from --bs-primary, so the overall bar
did not match the accent-green card bars. And Convert/Cancel were rendering
side by side, where the house pattern is for a primary action and its abort to
share one slot.
Window default widens to 1100x780; 700px fits only two cards per row.
core/display.js is dual-loaded - required by tests in Node, loaded as a plain
script by the renderer - so it takes no dependencies and receives the format
graph as an argument. Tests assert that property, that its STATUS mirrors
core/job.js, and that index.html loads it before renderer.js.
Selecting the "Choose format…" placeholder left the previous format in place. The per-card select passes null for the placeholder, and setTarget ran that through the same validity guard it uses for real formats — null is in no source's target list, so the guard skipped the job and the target was never cleared. The bulk bar had the same bug wearing different clothes: it tested `if (e.target.value)` before calling setTarget, so choosing the placeholder there did nothing at all. Clearing is now handled explicitly and skips the guard, which only ever applied to real formats. Two related cases fixed while in there: - Re-targeting a job that had already finished, failed or been cancelled left its old status on the card, so a converted file still read "Done" and a failed one kept a stale error even though the new target had not run yet. Changing the target now returns the job to pending and drops the old output path, so the card reads "Ready". - The bulk bar could set a target on a running job, whose select is disabled precisely to prevent that. Running jobs are now skipped. Toast icons and dismiss buttons sat 1.5px above the optical centre of the first line of text. material-icons ships line-height: 1, so an 18px glyph box shared its top edge with the body's 21px line box rather than its centre. Both now take the height of that line box and centre within it, measured at 0px offset across all four variants including a wrapped multi-line message — where the icon must stay on the first line rather than centring over all of them.
"1 still need a format" was the reported symptom. The audit found four such bugs, not one, and six more places doing it correctly but by hand: - "N still need a format" never agreed at one. - A single extracted frame read "1 frames". - A truncated folder scan read "1 files". - "skipped N unsupported" had no noun to agree with at all. The cause was that each call site wrote its own ternary, so getting it right depended on remembering to. display.plural() and display.countOf() now own this, and all thirteen counted strings go through them. Two tests guard the pattern: one rejects the `n === 1 ? "file" : "files"` shape anywhere in the UI code, the other rejects a raw interpolated count sitting directly before a plural noun. Writing the singular coverage surfaced a separate bug in the same area. The action bar showed its outcome summary only while `converting` was true, so the instant a run finished the bar snapped back to "1 file queued" — describing files that had just been converted as still waiting. It now switches on whether anything has actually settled, which also fixes the inverse case: a run with nothing finished yet reported "Nothing converted", which is true but reads like a failure, and now reads "Converting 3 files…". The detail line no longer claims "Ready to convert" after a completed run.
Four interface fixes plus the first manual testing checklist. **No UI text is highlightable.** This reverses a call made when the grid landed: filenames, metadata, status text and toasts were deliberately made selectable so an error could be copied. In practice a click-drag on a card caught the filename and highlighted it, which makes the card feel broken — selecting chrome is never what someone meant to do. Coverage is now complete, with real form fields the only exception. Copying an error is a job for a copy action on the context menu, not for making the whole interface selectable. **Cards respond to hover** with a background wash as well as a border tint, and a selected card that is also hovered keeps the stronger fill so it never reads as less selected than a merely hovered one. **Checkboxes in each card's top-left corner.** The whole card stays clickable, but multi-select was previously only discoverable if you already knew ctrl+click worked. A card's checkbox toggles just that card whatever else is selected. The bulk bar gains a select-all checkbox that shows an indeterminate dash on a partial selection. **Buttons that sit together now share a height.** Clear All and Cancel were btn-sm next to a full-size Convert, so Clear All looked stunted. All three measure the same, and the check is asserted rather than eyeballed. The bulk bar is now always visible once there are cards, which is the more substantial change. Feedback on the grid was that the previous version could convert a pile of files to one format in two clicks, and the first cut of the grid required selecting the cards first — making the most common case worse in order to serve the rarer per-file one. With no selection the bar reads "All N files" and applies to everything, so the two-click flow is back; selecting narrows the same control to the selection. Nothing per-card was given up. Adds docs/MANUAL-TESTING.md, a checklist for a human covering what the automated tests cannot judge: appearance in both themes, drag and drop, real dialogs, Explorer integration, installer behaviour, and the specific regressions each fix addressed. A section is appended per version from here on.
Two defects from the alpha.1 QA pass, plus the updater rule for pre-releases. **Escape now closes the Credits window.** It was handled by a keydown listener inside credits.html, which worked unpackaged but not once packaged — a page listener depends on that script having run and on focus sitting in the document, neither of which is guaranteed. It moves to before-input-event on the window's webContents in the main process, which sees the key regardless. The page handler is removed rather than kept as a fallback, so a regression in the main-process one cannot be masked. Verified against a packaged build by driving real Windows key input: Credits opens on its accelerator and closes on Escape. **A colliding output prompts instead of silently renaming.** The Cancel / Overwrite / Save as New dialog existed but was unreachable: main.js built each job without the user's conflict setting, so createJob's own default of "unique" applied and every collision quietly became "name (1).ext". The setting is now threaded through, and both it and createJob default to asking. The prompt gains an "Apply to all remaining files" checkbox. Jobs run through a concurrency pool, so without a way to answer once for the batch, converting fifty files into a folder that already holds them would have meant fifty modal dialogs. The choice is cleared when the pool drains so it cannot leak into the next run. **Pre-releases are never offered as an update.** This needed to be explicit: electron-updater turns allowPrerelease on by itself when the running version carries a pre-release tag, so shipping 2.0.0-alpha.1 would have opted every alpha tester into being updated to the next alpha. allowPrerelease, allowDowngrade and the channel are now pinned, and update-available re-checks the offer so a mis-tagged GitHub release cannot push an alpha at everyone. The manual update check was also wrong in a way that only shows up on a pre-release: it compared the found version to the running one for equality, so someone on 2.0.0-alpha.1 with a latest stable of 1.0.0 got neither "up to date" nor an offer — the check silently did nothing. core/version.js now owns the comparison, with tests covering pre-release ordering and the rule that only a strictly newer stable release is ever offered. Adds a QA-fixes section to the manual testing checklist covering all three, noting that the Escape case has to be tested against a packaged build.
The engine has supported codec choice, constant quality, bitrate, encoder presets, resize with fit modes, frame rate, trim, stream copy, output routing and name templating since the architecture slice. None of it was reachable. This is the interface for it. Every control is built from the target format's own capability descriptors, so a container is never offered a codec it cannot carry: WebM offers VP9 and not H.264, MP4 the reverse, MP3 only its own. Sections appear and disappear with the target — an audio target has no video controls, an image target has no audio, a still image has no trim while a GIF does. Invalid combinations are unreachable rather than merely rejected on submit. A live command preview runs the spec through the real argument builder in the main process, so what the dialog shows is what will actually be executed rather than a reconstruction that could drift from it. The dialog edits a *partial* spec. Anything left untouched is omitted entirely and createJob fills it from the format's defaults, so the dialog does not freeze today's defaults into every job. Applying with nothing changed leaves the job exactly as it was, and the card shows no settings summary. Deliberately not the only way to set a format. The per-card dropdown and the bulk bar still work, so "drop a folder, pick a format, go" stays two clicks — the point raised when the grid landed. Presets save and restore the whole configuration including the target format, through the store bindings that were already wired. Cards gain a tune button that opens the dialog for that one file, and a green summary line describing what has been configured. Three bugs found by driving the dialog rather than by reading it: - CRF was silently discarded whenever the codec select sat on "Format default". The effective codec is the chosen one *or* the container's default, and treating the empty select as "no codec" disabled constant quality and dropped the value on apply. - Switching to a codec with no constant-quality mode forced the control to bitrate and never released it, so returning to H.264 left it stuck and threw the CRF away again. - Both stream selects rendered blank. Assigning a value no option carries leaves selectedIndex at -1, which reads back as "" and collapsed the whole encode section, since those fields only show when the mode is "encode". Verified end to end against ffmpeg: a spec shaped exactly as the dialog produces one resizes, re-times, trims and downmixes the real output.
…ction bar Review feedback on the advanced settings dialog. **"Job" is gone from the interface.** One card opens **Advanced options**; a selection opens **Bulk edit**. The dialog titles itself according to how it was opened. Internal identifiers keep the word — core/job.js, JobRunner, the job:* channels, .job-card — because renaming them is a large diff with no user benefit and real regression risk, and none of it is visible. That is recorded as a rename-only commit if it is ever wanted. **Bulk edit moved out of the corner and into the green selection bar.** The bar already answers "what am I acting on", so the action that acts on it belongs beside the format dropdown rather than in a separate corner. It follows the same scope rule: the selection, or everything when nothing is selected, where it reads "Edit all". Options are already restricted to what every file in scope supports, since the target list is the intersection and every codec, quality and section is derived from that shared target. **"All 1 file" became "Select all"**, which is what the checkbox beside it actually does. **Trim is a two-point slider.** Two text boxes never showed the useful thing, which is where the kept span sits relative to the whole clip. Handles cannot cross, the kept duration is shown between them, and holding Shift while dragging scales pointer movement down for fine adjustment. Arrow keys move a handle, Shift+arrow moves it less, Home and End jump to the extremes. A file with no readable duration says it cannot be trimmed rather than presenting a slider with nothing to slide over, and a bulk edit spans the longest clip in the selection. **The dialog's Cancel was filled.** A neutral action beside a filled primary is outlined — two filled buttons side by side read as two equal choices, which is exactly what a cancel is not. The design system said the opposite, because Bootstrap's own modal example uses a filled btn-secondary and that is what the Dropgate client copied; the buttons and modal artboards now carry the rule and a correct/wrong pair. Bulk editing files with differing settings now starts blank and says so, rather than presenting one file's values as if they applied to all.
…ptions A fixed max-width on a <select> cuts the selected option off with no ellipsis and no scrollbar — the text simply disappears, which is how "Contain" was showing as "Contai" and "Constant quality" as "Constant qual". Both were my own inline max-widths. The rows they sit in now span the dialog grid rather than being constrained, and the selects keep a min-width floor so flex cannot shrink them below their own text. Rather than fix only the two that were spotted, this adds a sweep that measures every visible control against the text it is trying to show and fails if any is clipped: selects against their selected option, inputs against their value or placeholder, everything else on scrollWidth. It runs across six target formats, two window widths and both themes. It found exactly those two and nothing else, and now passes everywhere. Worth noting the narrow window was never affected — the dialog grid collapses to one column and gives each control more room, so this only showed at the wider layout where three columns compete. Presets are gone from advanced options. Advanced options are configured deliberately per file, because every file and workload differs, so there is nothing there worth saving. Reusable, saveable configuration belongs to pipelines: build one, then assign cards to it before starting. The preset:list|save|delete channels and their store bindings stay put — that is the persistence pipelines will use, so only the misplaced dialog UI was removed.
A card is configured by hand or handed to a saved pipeline, never both. Two tiles at the top of the advanced options dialog make that choice, and the sections below swap with it. Built on the Craftbox create-server pattern, with three deliberate departures. It is worth saying plainly that the pattern is not a radio group — that app has no radio inputs, no btn-check and no btn-group anywhere; it is tiles with role="button" and a click handler recomputing a selected class from one state variable. - **Theme-aware.** The original's idle border is a fixed white alpha, which only works because that app is dark-only. This uses the border token so it survives light mode. - **Strictly binary.** The original is tri-state: clicking the selected tile deselects back to "from scratch". That works there because a third state exists. With exactly two modes, deselecting would leave the form in no mode at all, so re-clicking the active tile does nothing. - **Keyboard operable.** The original has role="button" and nothing else — no tabindex, no aria-pressed, no key handling — so its tiles cannot be reached or activated from a keyboard at all. These carry all three plus a focus ring, and a locked tile leaves the tab order. Borrowed as-is: fail closed, then unlock. The Pipeline tile ships locked and explains why, and is enabled once a pipeline exists, the same way Craftbox locks Template until one is saved. The lock is enforced in the handler as well as the CSS — pointer-events stops a mouse but not a keyboard or a programmatic activation, which an integration check caught. Switching modes is lossless. Manual settings are retained while a card is in pipeline mode and restored on switching back, so comparing the two approaches costs nothing. Only the active side is ever used, and the card shows only the active side — a pipeline card names its pipeline rather than displaying dormant settings that will not run. A card whose pipeline has since been deleted reads "Pipeline missing" in amber, the same treatment as a card with no format, and blocks Convert. It does not fall back to the manual settings it still holds; converting with settings the user did not ask for is worse than refusing. The card field is called `processing`, not `mode`. core/job.js already uses job.mode for the conversion mode, and the card's fields are spread into the run spec, so a clash would have silently overwritten it. **Fixes a real bug in the pipeline compiler, found by running one for the first time.** Every stream was mapped with brackets, including untouched ones, so a passthrough audio stream came out as `-map "[0:a]"`. Brackets denote a filter_complex output label, so ffmpeg hunted for a label that did not exist and refused the output with "Error opening output files: Invalid argument". Only filter outputs are bracketed now. Two existing tests had encoded the buggy form and were corrected. Adds a mode-tiles artboard to the design system, since this is the first mode chooser in any of these apps and the next one should match it. The manual testing checklist gains a section for this, and every section now carries an explicit "not started" status. Nothing in that document has been executed — testing is on hold pending UX sign-off, and a written checklist must not be mistaken for a passed one.
The advanced options dialog, the selection-bar bulk edit, the clipping sweep and the manual/pipeline mode chooser have all landed since alpha.1. Nothing in docs/MANUAL-TESTING.md has been executed yet, so this build ships as an untested preview.
The first hands-on pass on real Windows reported nine issues. Seven reproduced
in the code and are fixed here; two did not, and are recorded as open rather
than papered over.
**Escape, Delete and Ctrl+A were swallowed after choosing a format.** The
global key handler treated any focused SELECT as "the user is typing" and bailed
out. A select keeps focus after you pick an option, so choosing a format on a
card ate the next key until something non-focusable was clicked. Narrowed to
TEXTAREA and text-bearing INPUTs — a checkbox is an INPUT too, so the type is
tested rather than just the tag. The dialog now also owns the keyboard while it
is open, or Delete removed the very cards being edited behind it.
**Convert re-ran finished batches.** startConversion took every card with a
format, so fixing one failed file and pressing Convert resubmitted the whole
completed batch and collided with every output the previous run had written.
It now skips cards that are already done; Retry is what re-runs one. The button
needed its own count, since `ready` includes finished cards and drives the
"still need a format" copy.
**"Apply to all remaining files" had no effect.** Not the flag — a race. The
concurrency pool brought several jobs to the resolver at once, and each read
the batch choice (still null) and opened its own dialog before the first answer
came back, so the tick could not reach the prompts already queued behind it.
Prompts are now serialised, and each re-reads the choice after the previous one
settles.
**One defect caused both reported Advanced-options bugs.** syncModalControls
rebuilds the codec selects from dataset.wanted, which was only written when the
dialog opened — so a pick was overwritten by the rebuild it triggered itself.
The codec was therefore dropped on Apply, which is worse than the cosmetic
label mismatch it was filed as. It also explains Quality appearing stuck on
"Target bitrate": with the select reset, the effective codec fell back to the
container default, and AVI's is mpeg4, which has no constant-quality mode. The
existing restore logic was correct and never got the chance to run.
**An unreadable file sat on "Reading…" forever.** probe() resolves { ok: false }
rather than rejecting, and describeMeta returned null for that exactly as it
does while a probe is still in flight. Pending and failed are now distinguished.
**DevTools were unreachable.** Electron's F12 accelerator comes from the default
application menu, so replacing it with a custom template removed it. devTools
was never disabled in webPreferences, so this was an accident rather than a
lock-down; re-declared explicitly.
**Two reported defects did not reproduce, and are not claimed as fixed.**
Both mechanisms were driven directly under Electron. Escape on the Credits
window: before-input-event fires and the window closes, with the webContents
holding focus. What was missing is a guard against opening the window more than
once — every menu click built another modal, so Escape closed the top one and
uncovered an identical one behind it, which reads exactly like Escape doing
nothing. That guard is added. Live theme change: the page's
prefers-color-scheme listener fires, matches flips and the page re-themes
itself. The OS-broadcast path cannot be exercised without changing the
machine's own theme setting, so a push from nativeTheme is added as a second
route; it is hardening for an unconfirmed report, not a reproduced fix.
The manual testing checklist gains a section covering all of it, including
steps aimed at establishing whether the two unconfirmed defects exist at all.
Nothing in that document has been executed since the first pass.
Ships the seven fixes from the first manual test pass. The checklist section for them is retitled to the release that actually carries them; nothing in that document has been executed since the pass that found them.
alpha.2 shipped everything downstream of a pipeline — the Manual settings / Pipeline tiles, the card's processing mode, the amber "Pipeline missing" state, and the IPC folding a saved graph into a job. What it never shipped was any way to make one, so the Pipeline tile has been locked in every released build with nothing able to unlock it. This closes that. A pipeline binds to one card and one input file, which is what keeps the editor a pure view layer: the compiler, the job model and the argument builder are all untouched. Joining several files together is a separate surface, so the palette omits Concatenate and Overlay — both still exist in the model and still compile, they simply cannot be added. The window gains a collapsible navigation rail. Files and Pipelines are siblings you switch between rather than a view you back out of, which is also what the Join surface will slot into. Collapse is remembered between launches. One function owns every route between views, so the unsaved-work guard cannot be sidestepped — a rail click, the link from the advanced options dialog and a file drop all pass through it, and a declined guard cancels the drop rather than ingesting behind it. core/pipeline.js gains, all additively: - The same UMD wrapper display.js uses, so the renderer loads it as a plain script. It requires nothing, which is what makes that safe, and a test now enforces that it stays that way. Validation and the command preview are synchronous as a result, with no IPC round trip. - A params schema on every node type, so the inspector is generated rather than hand-written per type. A second hand-maintained table of the same facts would drift from the first. - canConnect(), which refuses what the model tolerates but ffmpeg does not: a type mismatch, an input that is already connected (compile takes the first edge and drops the rest in silence), one output fanned into two nodes (a filter label can only be consumed once without split/asplit), a self-connection, and a loop. Each says why, in the app's own words, at the moment of the drop. - autoLayout(), placing nodes by dependency depth and leaving alone any the user has already moved. Node positions ride on the graph as node.ui, which validate and compile ignore and the store round-trips untouched. Also fixes a defect the editor exposes: validateJob ran before a pipeline was folded into the job, so an encode node naming a codec the target container cannot carry reached ffmpeg unchecked. Both the run and the preview paths now look again afterwards, and the card fails with a readable reason instead. Verified by driving the real app end to end — building a pipeline, saving it, assigning it to a card and converting, with ffprobe confirming the output is the size the pipeline described and that the untouched audio stream survived. The bracket invariant is checked there too: a filtered stream maps as [v0], an untouched one as 0:a. Manual testing steps for the rail and the editor are in docs/MANUAL-TESTING.md. The nav rail and node canvas patterns are recorded in the design system.
Pipelines went out in alpha.4 and are being taken straight back out. The feature was out of proportion to what this app is for — a node-graph editor is a large, fiddly thing to learn in order to do something the advanced options dialog already does per file — and in use it was unintuitive. The alpha.4 release and its tag have been deleted, so that version number is free again. Removed: the editor view, the Manual settings / Pipeline mode tiles, the graph model, compiler and validator in core/pipeline.js, the pipeline:* IPC channels and their store bindings, the card's processing and pipelineId fields, and the "Choose a pipeline" / "Pipeline missing" states in the display layer. A card is configured by hand, full stop. Kept deliberately: - The navigation rail, with Files as its only destination. Joining several files into one is still wanted and will sit beside Files rather than being somewhere you have to back out of. A rail with one destination costs a strip of width and promises nothing it cannot do, which a greyed "coming soon" item would. - The alpha.3 fixes that happened to live next to the removed code — the codec selects rebuilding themselves, the serialised conflict prompts, and the rest. The window minimum was 720x560, which let the selection bar run past the right edge and clip its own buttons. Measured rather than guessed: the file grid starts to break below about 800px wide. Now 880x600, which leaves headroom and still shows two full rows of cards. Two things the removal broke and this restores: the "Convert to" format row sat between the mode tiles and the Output section and went with them, and a leftover `manual` flag in syncModalControls stopped the trim slider being set up. Both were caught by driving the real app rather than by reading the diff. 126 unit tests pass. Verified end to end: the app starts with no console errors, nothing pipeline-shaped survives in the DOM or on the IPC bridge, the advanced options dialog opens with no chooser, and a real file still converts.
preset:list|save|delete were wired to electron-store in the 2.0 architecture
slice and never given a UI. They were kept through the pipeline work on the
grounds that pipelines would be their consumer; pipelines are gone, so nothing
is ever going to call them.
Removed with them: the "presets" key in the store defaults, and the {preset}
name-template token in core/paths.js. Nothing ever populated context.preset, so
that token silently expanded to an empty string — which is precisely the
behaviour the comment above it says the module avoids, since an unrecognised
token is meant to survive into the filename where a typo can be seen. It now
does, because {preset} is no longer recognised.
The encoder preset (-preset slow and friends) is a different thing entirely and
is untouched.
…cted **Drag and drop has never worked in 2.0.** Every drop threw "files is not iterable" before a single file reached the queue. getPathsForFiles took the whole FileList and iterated it inside the preload, but a FileList is not a type the context bridge can carry — it arrived on the other side as a plain object with no iterator. Individual File objects do cross intact, so the iteration now happens in the renderer, where the FileList is still a FileList, and the bridge resolves one file at a time. This predates the pipeline work and was not caused by removing it. It survived because the drop checks live in the alpha.1 section of the manual checklist, which has never been run, and because using the Add Files button takes a different path entirely and always worked. Reproduced before fixing, by dispatching a real OS-level drag through the debugger protocol rather than a synthetic event — a synthetic one carries no file the resolver can resolve, so it cannot show this bug. **The bulk bar now only offers actions that apply.** With nothing selected it showed an "Edit all" button, which is a whole-queue edit nobody asked for from a bar that is meant to be read as "here is what applies to everything". Bulk edit now appears only alongside Remove and Deselect, once a selection exists. The Select all checkbox, the "Convert all to" label and the format dropdown all stay put with no selection — that is the two-click flow and it must not gain a step. The button's label no longer changes, so its span id went with it.
…work
The queue described item 6 as "cross-kind conversions are not surfaced — remove
the sameKind filter". That premise was years stale: target lists have been
driven by the full capability graph since the card grid landed, and MP4 has been
offering all 21 targets, 14 of them cross-kind, with tests asserting it. The
filter only ever affected buildLegacyConversionMap, which the renderer uses to
decide whether a dropped file is supported and which icon to draw.
So the conversions were offered and then quietly did the wrong thing. This is
the correctness half; the frame-span UI follows.
An animated source keeps its motion into a video container. allowedModes mapped
image->video to ASSEMBLE on kind alone, so an animated GIF asked for MP4 was
treated as one still and looped for five seconds — the animation discarded.
Worse, ASSEMBLE emits -loop, which belongs to the image2 demuxer; the gif
demuxer has -ignore_loop instead. An animated source now transcodes.
Whether a file moves is decided by the probe, not by its extension. gif and webp
*may* hold an animation and usually do not, and allowedModes can only see two
extension strings. An ordinary still WebP asked for PNG therefore took the
animated path and landed as a *directory* containing one frame. createJob now
resolves the default mode from inputMeta when it has one, and main re-resolves
after its own probe.
An image source is never asked to carry audio. The old check tested for
ASSEMBLE, which stopped covering an animated GIF the moment one transcoded.
A single frame is taken from the middle of the video rather than from frame
zero, which is a fade-in or a black slate often enough to be the wrong default
for something whose whole purpose is to represent the clip.
Frame files are numbered %06d. ffmpeg widens the field rather than truncating,
so %04d ran frame_9999 straight into frame_10000 and the directory stopped
sorting in capture order exactly when there were enough frames to care.
Choosing frames out of a video is offered as a span. applicableSections hid the
trim control for every still target, which is backwards: a PNG from another
picture has nothing to choose, but a PNG from a video is entirely a question of
which frames. It now takes the source and the mode, not just the target.
Stream copy is offered only when it would work. Every audio container claims
stream-copy support, so extracting audio always offered "copy without
re-encoding" — which fails inside ffmpeg unless the stream already fits the
target. The check needed an encoder-to-codec mapping: the format table names
encoders, a probe names codecs, and ffmpeg's own -encoders output spells out the
three that differ ("libmp3lame ... (codec mp3)").
Retargeting across kinds drops settings chosen for the old target. Verified
before the fix: a card configured with libx264 and retargeted MP4 -> PNG failed
with "PNG does not support the video codec libx264", a setting the user never
asked to apply to a picture.
Also removed, all unreachable: job.inputIsSequence (read by buildArgs, never set
by createJob, so assembling a numbered sequence could not happen — it needs
several cards to become one job, which is item 5's problem), job.extraArgs (the
pipeline compiler's channel onto a job), STATIC_IMAGE_EXTS, and the legacy map's
same-kind targets array, which no longer had a reader and implied the UI was
still same-kind only.
144 tests pass, 18 of them new, including a matrix guard over all 867
source/target/metadata combinations asserting that display.js's effectiveMode
and core/job.js's defaultModeFor never disagree — display.js cannot require the
job model, so the two are policed the same way STATUS is.
The second half of cross-kind conversions. The first made them correct; this makes the one with a real decision in it usable. Video into stills is the only conversion where the mode genuinely matters — every frame, or one — and it was unreachable. Every target descriptor carried a `modes` array the renderer never read, so video to PNG was always *all* frames and THUMBNAIL could not be reached at all. There is no mode dropdown. The trim slider becomes a span with a decoded frame shown at each end, and the choice between every frame in that span and a single one *is* the mode decision. applicableSections already knew the target; it now knows the source and the mode too, because the same target means different things depending on where it came from: a PNG from another picture has nothing to choose, a PNG from a video is entirely a question of which frames. Frame previews come through a new preview:frame channel — one decoded frame at a timestamp, as a data URL. Debounced and token-guarded, because dragging a handle asks for a great many frames in quick succession and a slow decode landing late must not paint a frame the user has moved past. A source that will not decode simply shows nothing; a missing preview must never block the choice. The preview of the far end seeks just inside the duration. The end handle sits exactly at it, which is past the last frame, and decodes nothing at all. Frame extraction could write thousands of files from one click with no warning. The card now states the count, and Convert asks once above a thousand. The count is the honest lever too: narrowing the span is how you make it smaller, which is why the card's line opens the dialog. A picture target no longer asks for a video codec, a target bitrate or an encoder preset. Exporting frames to PNG rendered the whole encoder block above the controls that decide anything — the codec dropdown holding exactly one entry. Resize and frame rate still apply, so this is narrower than hiding the section, and it is titled Picture rather than Video. Two bugs found by driving the real app rather than reading the diff. setupTrim only ran when sections.trim was true, which a frames conversion sets false — the slider was never wired up at all. And compactSettings dropped any top-level value that was not an object, so the scalar `mode` was silently thrown away on Apply and the choice never reached the job. 145 tests pass. Verified end to end against real ffmpeg output: a narrow range writes exactly that range in sortable order, a single frame is one file with real content in it, and the count on the card matches what lands on disk.
Three fixes from the cross-kind test pass. **The frames readout said "keeping" until something moved.** framesMode() read back a CSS class that renderFramesChoice sets, but the slider paints first — so on the first open the class still described the previous conversion and the readout used trim wording for a frame range. It is held as state now, set before anything paints, rather than recovered from the DOM. **A failure reported ffmpeg's verdict instead of its reason.** lastMeaningfulLine took the last non-empty stderr line, which is reliably "Conversion failed!" — ffmpeg always closes with it. The genuine explanation sits several lines earlier, behind the progress counters and a line describing the consequence rather than the cause. It now skips those and prefers the first line that names a cause, stripping the "[component @ 0x7f...]" prefix that means nothing to a user. An animated WebP now reports "skipping unsupported chunk: ANIM" rather than nothing at all. **An animated WebP is refused before the conversion is configured.** The bundled FFmpeg 6.1.1 cannot read one — its WebP decoder skips the ANIM/ANMF chunks — and ffprobe describes such a file as 0x0. A picture stream with no measurable dimensions is one ffmpeg cannot decode, so the probe reports it as unreadable with a reason and the card says so as soon as it returns. This is generic rather than WebP-specific: an audio file has no picture stream to measure, and cover art has real dimensions, so neither is affected. A third fix, found while testing the first: isSingleFrame() consulted the frames choice even when the conversion had none, so changing a target from PNG back to MP4 left the readout blank — and would have thrown away the trim's end, since a single frame deliberately carries no span. The manual checklist claimed animated WebP to PNG should produce a folder of frames. It cannot, and would have sent the next tester after a defect that is not ours; corrected, with the limitation recorded in the queue against any future ffmpeg bump. 152 tests pass. Verified in the real app: the readout is right on first open, every cross-kind conversion still produces what it did, and an animated WebP is caught at probe time rather than at conversion.
Everything that moves within its current major, taken without ceremony:
electron-builder 26.8.1 -> 26.15.3, electron-updater 6.8.3 -> 6.8.9.
Electron went 41.1.0 -> 44.0.0, three majors, bringing Chromium 152 and Node 24.
**No application code changed for it.** That was established by evidence rather
than nerve: unit tests pass on any Electron and prove nothing about a runtime
bump, so the real app was driven against the things a major version actually
threatens.
- webUtils.getPathForFile across the context bridge, exercised with a real OS
drag dispatched through the debugger protocol rather than a synthetic event —
the distinction matters, since a synthetic drop carries no file the resolver
can resolve and would pass while broken.
- Spawning ffmpeg and piping a decoded frame back over IPC for the previews.
- Bootstrap's modal machinery, via the frame-count confirmation.
- Live theming, and every cross-kind conversion end to end with the output
inspected by ffprobe.
The one deprecation that surfaced is in test tooling, not the app:
webContents.on("console-message") now passes an event object, and the old
(event, level, message) signature warns.
The installer is about 308MB, up from 242MB. That is Electron 44's own growth
and wants saying in the release notes rather than being discovered.
allowScripts is kept, and re-keyed against what is installed — it pins exact
versions, so it goes stale the moment a dependency moves and the lifecycle
prompt comes back.
Deliberately not touched: the transitive @electron/* packages (asar 3->4, get
2->5, notarize 2->3, universal 2->3, fuses 1->2). They belong to app-builder-lib
and electron-winstaller, and forcing them ahead of their parents breaks a build
for no benefit. They move when electron-builder moves.
152 tests pass and the installer builds and verifies. A manual section is added
for the runtime change; the same sweep is queued for the other projects.
The alpha.1 and alpha.2 sections had been held at "not started" since 2026-08-28, waiting for the UX direction to settle before anyone spent a pass on a surface that might still move. It has settled, so they were worked through against alpha.4 along with the three alpha.4 sections. Every section now carries a PASSED status and every box is ticked, 307 of them. Two items are worth naming because their own prose said they had never been run, and that prose is now wrong: drag and drop from Explorer, which automation cannot drive out of a restricted Explorer window and which had been flagged and deferred repeatedly, and converting end to end from the installed build, which needed a reinstall from the clean 2026-09-02 installer because the copy installed before it was the polluted 08-31 build. Both are recorded against the date and the person who ran them rather than being silently ticked. The withdrawn "Processing mode (Manual settings / Pipeline)" section is deleted rather than left in place, 76 lines of it. It was kept after 09eaca3 as a record of what was tried, but it describes tiles, a chooser and a compiled-pipeline command preview that no longer exist anywhere in the app, and a checklist is read by someone looking for work to do. The record of why pipelines went belongs in that commit and in the release notes, not in a list of steps nobody can follow. The file's own rule is unchanged and still at the top: a written checklist is not a passed one, and no build should be described as tested on the strength of this file existing.
A job waiting on the "If it already exists" prompt was in neither `active` nor `queue`, so the pool could not see it at all. Three defects follow from that one fact, and they are fixed by giving the job a slot to hold while it waits rather than by patching each symptom. **"Apply to all remaining files" was discarded mid-batch.** _checkIdle tests active.size and queue.length, both zero when every job is parked at a prompt, so the runner announced it had finished while the user was still reading the first dialog. main.js clears the batch-wide choice on idle. Measured before the fix: five files at concurrency two, one tick of the checkbox, four further dialogs. This is the same symptom a tester reported against alpha.1. e4e16e5 fixed the original — main built the job without the user's setting, so createJob's own default of "unique" applied and every collision silently became "name (1).ext" — and 4f1d06b fixed a race after it. Both were real. This is a third route to the same place, and it survived because the checklist case for it uses six similar files, which all finish probing before the first answer. That is the one ordering that works. **Concurrency did not apply to anything that took the ask path.** Nothing re-checked the pool between the answer and the spawn, so once a batch answer was set every parked job spawned in the same tick: eight ffmpeg processes at a limit of two. **Cancel All during a prompt still ran the conversion.** _cancelledAll was read on entry and never again, so answering the dialog spawned ffmpeg regardless and the job was marked cancelled in the close handler — after it had overwritten the file. The reservation makes _pump mandatory on the two pre-existing early returns after the await. They got away with not pumping while a parked job held no slot; now one does, missing it would stall the queue for good. There is a test for that specifically, because it is a regression this change introduces rather than one it fixes. Left alone deliberately: cancel(jobId) for a job at a prompt still returns false and does nothing. The dialog is OS-modal and cannot be dismissed from here, so there is genuinely nothing to cancel until it is answered. Now commented rather than silent. Also drops the renderer stripping onConflict === "ask" before sending. It is free today because the app-wide setting is "ask" too, so main reinstates the same value. It stops being free the moment a global conflict preference exists: a card set explicitly back to Ask me would inherit a global Overwrite instead. Routing keeps its strip, and the asymmetry is commented. Tests: five behavioural ones in a new test/runner.test.js, driving a real JobRunner with an injected spawn. Confirmed to fail against the old code before the fix, which is the only way to know a concurrency test is testing anything. Two tests deleted rather than kept — both read main.js as a string, and one asserted the presence of the very line that caused the first defect, reading it as proof the behaviour was correct. Prompt ordering needs a real dialog and moves to the manual checklist. The wiring guard that main still threads the setting through stays, retitled to say what it is. 155 tests pass.
Dropping a folder reported "skipped 2 unsupported files" and nothing else,
so you knew something had gone missing but not what, and not whether it was
the app's limitation or a filter of your own. v1 discarded them in silence
and that was worse, but a bare count is not much of an answer.
scanPaths now returns skipped as { path, reason } entries rather than a
counter, matching the shape errors already used. Three reason codes -
unsupported, not-included, excluded - and no sentences: this module has no
business deciding how any of it is worded. A file reached twice is still not
counted as skipped, because nothing was dropped and there is nothing to say.
The toast gained an optional action, and the skipped list is that action
rather than a panel of its own. A panel would need a position, a dismissal
model, a focus model and an Escape handler in order to report that four
files were ignored - and the house rule is that anything merely reporting
what already happened is a toast. Filenames in the list override the
app-wide user-select: none, since a filename you cannot copy is not much use
in a bug report. Everything is still set through textContent; a toast
carries filenames and ffmpeg's own words, and neither is markup this app
gets to trust. A toast with an action no longer times out by default.
Two reporting bugs go with it, both from the branches being an else-if
chain over conditions that are not mutually exclusive:
- A scan that both truncated and skipped reported only the truncation. The
clauses now compose, so it reports both in one sentence.
- Adding files that were every one of them already queued said nothing
whatsoever, which is indistinguishable from a drop that never registered.
It now says so.
And there were two different "skipped" toasts - one in the scanner path, one
in addFiles' own format check - which could both fire for a single folder
and word the same thing differently. addFiles no longer reports anything; it
returns { added, rejected, duplicates } and the caller merges its rejections
with the scanner's into one message, because they are one thing to the
person reading them.
Tests: reason codes per skip, a filtered-out file distinguished from an
unreadable one, and a dedupe asserted not to count as a skip. The existing
scan test is updated for the new shape - it was the only other consumer.
156 tests pass.
scanPaths has supported a recursion switch, extension filters, a symlink switch, a depth limit and a file cap since the 2.0 architecture slice landed. None of it was reachable: ingestPaths passed a literal empty object, and had done since the day it was written. Three of them are now on a popover beside Add Folder - include subfolders, which kinds to add, and whether to follow shortcuts. They persist in the settings store, which already shallow-merges whatever it is given, so this needed no new IPC and no new channel; the options parameter was already threaded renderer to preload to main to scanner. Add Folder keeps its own click and opens the picker as it always did. The options get a separate caret beside it. Adding a folder is the two-click path and it does not get to grow a step because a setting exists. maxDepth and maxFiles are deliberately not offered. They are safety rails, not preferences - nobody opens a popover to decide how deep sixteen levels is - and the toast that fires when one of them bites explains itself at the moment it matters, which a number sitting in a menu cannot. The options apply to every ingest route rather than only the button, because a plain drop can contain folders too and having a setting cover one folder route but not the other is indefensible. The cost is that a dropped .jpg is skipped while Images is off, which is only acceptable because the previous commit made the toast name that as the reason instead of dropping it in silence. Unticking the last remaining kind is refused. A filter admitting nothing would skip every file in the folder and then carefully explain why, which is honest and useless. Also fixes the queue's dedupe, which compared raw path strings. Two spellings of one Windows path are the same file - the separators can go either way and the case is not significant - and the scanner has always keyed its own dedupe that way. So a file already queued by a folder walk could be queued a second time by being dragged in directly, giving two cards for one file. Found by driving the real app rather than by reading the diff: the folder walk produces backslash paths and the direct add did not, and the new duplicate counter reported them as newly added. Verified in the running app: the popover opens clear of the window edge and closes on an outside click and on Escape, unticking Images skips a .png with the reason "turned off in folder options" rather than "not a supported format", unticking the last kind refuses, and one file reached three different ways produces exactly one card. 156 tests pass.
"Mirror the source folders" has been in the Destination dropdown since alpha.2 and has never worked in any released build. paths.js resolves it from output.mirrorRoot, validateJob rejects a mirror job that has no root, and nothing anywhere ever set one - so choosing it replaced the command preview with "Mirrored output was selected but no source root was given" and disabled Apply, with nothing the user could do to proceed. scan.commonRoot has existed and been tested since the architecture slice, with zero callers. This is the join that was missing. The root is the folder that was handed in, not the common ancestor of the files that came back. Those are the same only when the folder has more than one populated subtree. Point at a folder holding just 2024/holiday/clip.mp4 and the ancestor of the found files is 2024/holiday, so the mirror would flatten away the two levels the user asked to keep - the failure would look like the feature working, on the folders where it matters least. So scanPaths now reports the root itself: the directory it was given, or the files' shared ancestor when it was handed loose files and there is no better answer. commonRoot is now a thin wrapper over a commonDir that works on directories, which keeps its signature and its existing test intact. The root rides on the card rather than in job.settings, because applyJobModal writes one settings object across a whole selection while the root belongs to the ingest that produced each card. It is threaded at both send sites - the run and the preview. Missing the preview would leave Apply disabled, which is the original bug wearing a different hat. Where there is no root at all - cards from folders on different drives - mirror is disabled in the dropdown and says why, rather than being offered and then failing validation. A file added on its own keeps it: its own folder is a perfectly good root, and the output simply lands flat. Verified end to end in the running app against real ffmpeg: a folder containing 2024/holiday/clip.mp4 previews ok, converts, and lands at mirror-out/2024/holiday/clip.mkv, confirmed with ffprobe as a real 160x120 h264 file rather than an empty one. Before the fix the same steps could not get past the disabled Apply button. 158 tests pass, including that a scanned root satisfies validateJob and resolves to the rebuilt directory - the seam where these two halves failed to meet - plus the cross-drive null and drive-root cases, which were both untested.
…nd commit the lock file The kit was a packed tarball while it was private. It's on npm now, and pinned to exactly 0.1.0-beta.1, so a fresh install can't move to a later beta before it's been tried here. Its code is the same as the tarball's, file for file. package-lock.json is committed from now on, so every install and build gets the same versions, and CI can run npm ci.
… FFmpeg is missing npm 12.1 no longer counts a version-pinned allowScripts entry (ffmpeg-static@5.3.0) as approving that package, though 5.3.0 is what's installed, so a fresh install on Linux never downloaded FFmpeg. The tests that need it skipped, and electron-builder built an AppImage and a .deb without it, saying nothing. The approvals are now by name, the form npm 12.1's own install-scripts approve writes; npm 12.0.2 runs them the same way (checked). electron@44.0.0 goes: Electron 44.4 has no install script. CI now checks that FFmpeg and FFprobe are there after npm ci.
2.0.0-beta.3: on @diamonddigitaldev/electron-kit
…eps for Windows and Linux package.json's description said "An Electron-based Windows app", and it's what the .deb and .rpm show as their description; it now matches the README's own tagline. The README's platform badge said Windows only, though 2.0 builds for Linux too. Installation now has a part per system, as 2.0.0-beta.3's release notes do: the Windows installer and its SmartScreen note; the .deb, .rpm and AppImage, with what doesn't work on Linux yet; then what's bundled, what the updater's files are, and how to go back.
Call it a desktop app everywhere, and give the README installation steps for Windows and Linux
2.0.0-beta.4, on electron-kit 0.1.0-beta.3
…onverting card - Cancel no longer flickers under the pointer: a card's buttons are only rebuilt when they change, not on every progress tick. - The Last frame preview steps back when its seek lands after the video's last picture, as it does when the audio runs longer than the video. - A card in a running batch, queued or converting, locks its Advanced Options button, its image-count line and its format select. The dialog used to open and report settings applied that never took effect. 2.0.0-beta.5, with its manual testing section.
Fix three card bugs, 2.0.0-beta.5
…ux, appId and busy
- electron-builder.cjs, from config(): package.json's build moves there, with the 22 types as fileTypes and
"Convert with Diamond File Converter" on files and folders as contextMenu. installer.nsh goes: the kit's
installer writes the same key, asks who it's for (upgrading an install for everyone in place), and asks for
each type and the right-click entry. On Linux, the .deb and .rpm put the app in Open With for each type.
- start({ appId }): Windows groups the window under its pinned icon and shows its notifications.
- mountShell({ busy }): Restart Now asks first while a conversion or a join runs.
- README's installation, the design-system note, and a beta.6 section in MANUAL-TESTING.md.
2.0.0-beta.6: the kit's config() and installer, on electron-kit 0.1.0-beta.8
… ends converting and joinRunning were cleared only when the run resolved; a run that threw left Restart Now asking about a conversion that wasn't running.
Once a card had converted, its format and Advanced Options could still be changed, but nothing would run them: Convert skips a Done card, and only a failed card offered Retry. A converted card now keeps the format and options that made its file, locked as a card in the run is, with Convert Again beside Show in Folder. Convert Again sends it back to Ready with the same format and options, unlocked, and Convert runs only it. The earlier file is left where it is: a run onto the same name meets File Already Exists, as any other does. The lock, the reset and the card's buttons move into display.js, where the suite can reach them. The bulk bar's format and Bulk Edit are disabled when every card they would apply to is locked, rather than taking a pick and changing nothing.
…'t delete it
Converting a file to its own format onto itself sent FFmpeg straight at its
own input, which it refuses ("cannot edit existing files in-place"). The
clean-up after a failed run then deleted the output path, which was the
original. Any overwrite whose run failed or was cancelled lost the old file
the same way.
A run onto something that already exists now writes name.dfc-partial.ext
beside it and renames it over the target only once FFmpeg exits cleanly.
Each clip's Trim button opens the span control under its row: the slider, the exact-time boxes, the first and last frames, and Shift for one frame at a time. The drag and keys (wireTrimHandles) and the frame painter (framePreviewer) are now shared with Advanced Options rather than copied.
Restart Now's busy question is cleared however a conversion or a join ends
Convert Again, overwrite that can't delete the file, and Join's trim (2.0.0-beta.7)
…t runs The clip list goes inert and grey, and Join into, File name and Browse are disabled, until the join ends however it ends. A file dropped on Join mid-run isn't added.
The lock made the whole scrolling area inert, which took its scrollbar and mouse wheel with it, so a long list couldn't be looked through mid-join. Now the clips and Add More are inert and grey; the scroller stays live.
Join's clip list scrolls while a join runs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Diamond File Converter 2.0: a rebuilt conversion architecture with a context-isolated renderer, a job card grid replacing the old queue list, batch and folder ingest, frame trimming, clip joining, and a run of manual QA fixes.
Test plan