diff --git a/docs/MANUAL-TESTING.md b/docs/MANUAL-TESTING.md
index 6cc3d57..e701f05 100644
--- a/docs/MANUAL-TESTING.md
+++ b/docs/MANUAL-TESTING.md
@@ -29,7 +29,7 @@ sections — the history is useful.
```
npm install
-npm test # expect 198 passing, 0 failing
+npm test # expect 207 passing, 0 failing
npm start
```
@@ -2876,3 +2876,43 @@ Two things Will found on beta.6:
- [x] Look at all of it in both light and dark themes.
- [x] Advanced Options' trim on the Convert view still drags, Shift-drags and
shows its two frames as before.
+
+## Join says how far it is, and locks while it runs (2026-10-16)
+
+Two things Will found on beta.7:
+
+- **Join's bar had no words.** It moved, but nothing said how far. It now has
+ **Joining 42%** under it, left-aligned, and **Joining…** while the length
+ isn't known (the bar slides then).
+- **Join's settings could be changed mid-join.** The clip list (order, trims,
+ Remove, Add More) and Join into, File name and Browse all stayed live while
+ a join ran, though nothing changed then would take effect. Now they're
+ greyed out and locked until it ends, however it ends. A file dropped on Join
+ during a run isn't added: a toast says to wait for the join to finish.
+
+> **Status: not yet run by hand.** Driven once in the running app on
+> 2026-10-16 (two clips needing a re-encode: the label read "Joining 48%",
+> left-aligned; the list was inert and greyed, the side's fields disabled, a
+> click on Remove did nothing; all of it came back after the join, with the
+> first row's Move Up still disabled).
+
+### Joining XX%
+
+- [ ] In Join, add two videos of different sizes (so they re-encode, which
+ takes long enough to watch), choose a folder, press **Join**. Under the
+ bar, on the left, it reads **Joining 0%** and counts up to the end.
+- [ ] When the join ends, the bar and its text go away.
+
+### Locked while joining
+
+- [ ] Open one clip's **Trim**, then press **Join**. While it runs, the list
+ is greyed out: Trim, the slider, Start and End, Move Up and Down, Remove
+ and **Add More** do nothing, and Tab doesn't reach them.
+- [ ] **Join into**, **File name** and **Browse** are greyed out and can't be
+ changed. **Clear** is disabled; **Cancel** works.
+- [ ] Drop a file on Join while it runs: it isn't added, and a toast says to
+ wait for the join to finish.
+- [ ] When it finishes, everything works again, and the first clip's Move Up
+ and the last clip's Move Down are still disabled.
+- [ ] Do it again and press **Cancel**: everything unlocks the same way.
+- [ ] Look at it in both light and dark themes.
diff --git a/package-lock.json b/package-lock.json
index 338ef5e..a6174fc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "diamond-file-converter",
- "version": "2.0.0-beta.7",
+ "version": "2.0.0-beta.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "diamond-file-converter",
- "version": "2.0.0-beta.7",
+ "version": "2.0.0-beta.8",
"license": "Apache-2.0",
"dependencies": {
"@diamonddigitaldev/electron-kit": "0.1.0-beta.8",
diff --git a/package.json b/package.json
index 401c22e..48a833f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "diamond-file-converter",
- "version": "2.0.0-beta.7",
+ "version": "2.0.0-beta.8",
"description": "An Electron-based desktop app for converting audio, video, and image files.",
"main": "src/main.js",
"funding": {
diff --git a/src/index.html b/src/index.html
index 79fcb3e..682cbcc 100644
--- a/src/index.html
+++ b/src/index.html
@@ -141,7 +141,10 @@
-
+
+
+
+
diff --git a/src/renderer.js b/src/renderer.js
index a577893..f05090f 100644
--- a/src/renderer.js
+++ b/src/renderer.js
@@ -1645,6 +1645,11 @@ let joinTrimOpen = null; // the id of the clip whose trim is open, if any
*/
async function addJoinPaths(inputPaths) {
if (!inputPaths || inputPaths.length === 0) return;
+ // The list is what's being joined, so it holds still until the join ends.
+ if (joinRunning) {
+ kit.ui.toast("Wait for the join to finish before adding more files.", { type: "warning" });
+ return;
+ }
try {
const result = await api.scanPaths(inputPaths, buildScanOptions());
const added = await addJoinClips(result.files);
@@ -2052,7 +2057,30 @@ function renderJoinActions() {
$("btn-join-run").classList.toggle("d-none", Boolean(joinRunning));
$("btn-join-cancel").classList.toggle("d-none", !joinRunning);
$("btn-join-clear").disabled = Boolean(joinRunning);
- joinProgress.element.classList.toggle("d-none", !joinRunning);
+ renderJoinLock();
+ $("join-progress").classList.toggle("d-none", !joinRunning);
+}
+
+/**
+ * While a join runs, everything that configures it is greyed out and locked:
+ * the clip list (order, trims, removing, Add More) and the side's fields. The
+ * list is made inert rather than each control disabled, so the rows keep their
+ * own disabled states (the first row's Move Up) for when it unlocks.
+ */
+function renderJoinLock() {
+ const locked = Boolean(joinRunning);
+ const scroll = document.querySelector("#join-body .join-scroll");
+ scroll.inert = locked;
+ scroll.classList.toggle("is-locked", locked);
+ for (const id of ["join-format", "join-name", "btn-join-browse"]) $(id).disabled = locked;
+}
+
+/** Join's progress: the bar, then "Joining 42%" under it, left-aligned. */
+function setJoinProgress(percent) {
+ joinProgress.set(percent);
+ $("join-progress-text").textContent = typeof percent === "number"
+ ? `Joining ${Math.round(Math.min(100, Math.max(0, percent)))}%`
+ : "Joining…";
}
async function runJoin() {
@@ -2078,7 +2106,7 @@ async function runJoin() {
joinRunning = generateId();
spec.id = joinRunning;
- joinProgress.set(0);
+ setJoinProgress(0);
renderJoinActions();
// However it ends, as a conversion's.
@@ -2106,8 +2134,7 @@ let joinProgress = null;
function setupJoin() {
joinProgress = kit.ui.progress({ label: "Joining" });
- joinProgress.element.classList.add("join-progress", "d-none");
- $("join-progress").replaceWith(joinProgress.element);
+ $("join-progress").prepend(joinProgress.element);
// The same drop zone as Convert's, from the kit, so the two sections read
// as one app: only the icon and the label change. Files dropped anywhere
@@ -2367,7 +2394,7 @@ api.onJobProgress(({ jobId, percent }) => {
// A join is a job too, and it is the only one with no card to land on.
if (jobId === joinRunning) {
// null when the length isn't known: the bar slides instead of reading "null%".
- joinProgress?.set(typeof percent === "number" ? percent : null);
+ if (joinProgress) setJoinProgress(typeof percent === "number" ? percent : null);
return;
}
const job = jobs.find(j => j.id === jobId);
diff --git a/src/styles.css b/src/styles.css
index 8c0b5e3..eb23e0e 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -665,6 +665,10 @@ a {
.join-plan-why { margin: 0.35rem 0 0; opacity: 0.75; }
.join-progress { margin-top: 0.9rem; }
+.join-progress-text { margin-top: 0.35rem; font-size: 0.8rem; text-align: left; opacity: 0.75; }
+
+/* While a join runs the list can't change, and looks it. */
+.join-scroll.is-locked { opacity: 0.5; transition: opacity var(--dur-state) var(--ease-state); }
.join-actions { margin-top: auto; padding-top: 0.9rem; display: flex; gap: 0.5rem; justify-content: flex-end; }
diff --git a/test/kit.test.js b/test/kit.test.js
index 9071723..b188464 100644
--- a/test/kit.test.js
+++ b/test/kit.test.js
@@ -161,10 +161,23 @@ test("what Restart Now asks about is cleared however a conversion or a join ends
test("progress of null is \"not known\": a card's and Join's bars slide, never sit at 0% or read null%", () => {
const renderer = read("renderer.js");
assert.match(renderer, /progress\.set\(typeof job\.progress === "number" \? job\.progress : null\)/);
- assert.match(renderer, /joinProgress\?\.set\(typeof percent === "number" \? percent : null\)/);
+ assert.match(renderer, /setJoinProgress\(typeof percent === "number" \? percent : null\)/);
assert.ok(!/style\.width = `\$\{percent\}%`/.test(renderer), "Join's bar used to read \"null%\"");
});
+test("Join's bar has \"Joining XX%\" under it, and its settings lock while it runs", () => {
+ const renderer = read("renderer.js");
+ const html = read("index.html");
+ assert.match(html, /id="join-progress-text"/);
+ assert.match(renderer, /`Joining \$\{Math\.round\(/);
+ assert.match(renderer, /: "Joining…";/);
+ // The list goes inert and grey; the side's fields are disabled.
+ assert.match(renderer, /scroll\.inert = locked;/);
+ assert.match(renderer, /\["join-format", "join-name", "btn-join-browse"\]\) \$\(id\)\.disabled = locked;/);
+ // A drop during a join doesn't change what's being joined.
+ assert.match(renderer, /if \(joinRunning\) \{\r?\n\s+kit\.ui\.toast\("Wait for the join to finish/);
+});
+
test("every icon is hidden from screen readers, and every icon-only button has a name", () => {
const html = read("index.html");
const spans = html.match(/