settings: unify transcode param styling and controls

- Style QP <input type=number> like the <select>s (it rendered with the
  default white background -> stood out); add a disabled (greyed) style.
- QP vs bitrate are now mutually DISABLED instead of hidden: QP mode
  greys the bitrate field, VBR/CBR greys the QP field. Hiding never
  worked because .tr-row label{display:flex} overrode [hidden].
- Replace SOFT/MEDIUM/HARD preset buttons with a "Готовый профиль"
  dropdown, placed last (rightmost) in the params row.

VERSION -> 0.0.120.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 17:21:28 +05:00
parent c876a46e61
commit 44f5aa920f
3 changed files with 31 additions and 33 deletions
+1 -1
View File
@@ -1 +1 @@
0.0.119 0.0.120
+27 -31
View File
@@ -1452,20 +1452,20 @@ const Settings = {
trRowHtml(pr) { // ряд параметров одного профиля trRowHtml(pr) { // ряд параметров одного профиля
const sel = (f, opts) => `<select data-f="${f}">` + opts.map(([v, t]) => const sel = (f, opts) => `<select data-f="${f}">` + opts.map(([v, t]) =>
`<option value="${v}"${String(pr[f]) === String(v) ? " selected" : ""}>${t}</option>`).join("") + `</select>`; `<option value="${v}"${String(pr[f]) === String(v) ? " selected" : ""}>${t}</option>`).join("") + `</select>`;
const active = matchPreset(pr); // «Готовый профиль» SOFT/MEDIUM/HARD — выпадающее меню; выставляет все параметры разом
const presets = `<div class="tr-presets"><span class="muted">Пресет:</span>` + const qmatch = matchPreset(pr);
Object.keys(TR_PRESETS).map((k) => const qpresetSel = `<select class="qpreset">` +
`<button type="button" class="tr-preset${k === active ? " active" : ""}" data-preset="${k}">${k}</button>`).join("") + [["", "— свой —"]].concat(Object.keys(TR_PRESETS).map((k) => [k, k])).map(([v, t]) =>
`</div>`; `<option value="${v}"${v === qmatch ? " selected" : ""}>${t}</option>`).join("") + `</select>`;
return ( return (
presets +
`<label>Кодек${sel("codec", [["h264", "H.264"], ["vp9", "VP9"]])}</label>` + `<label>Кодек${sel("codec", [["h264", "H.264"], ["vp9", "VP9"]])}</label>` +
`<label>Профиль H.264${sel("profile", [["baseline", "baseline"], ["main", "main"], ["high", "high"]])}</label>` + `<label>Профиль H.264${sel("profile", [["baseline", "baseline"], ["main", "main"], ["high", "high"]])}</label>` +
`<label>Контроль битрейта${sel("rc", [["vbr", "VBR (переменный)"], ["cbr", "CBR (постоянный)"], ["qp", "QP (постоянный)"]])}</label>` + `<label>Контроль битрейта${sel("rc", [["vbr", "VBR (переменный)"], ["cbr", "CBR (постоянный)"], ["qp", "QP (постоянный)"]])}</label>` +
`<label>Битрейт${sel("bitrate", [["500k", "0,5 Мбит/с"], ["1000k", "1 Мбит/с"], ["2000k", "2 Мбит/с"], ["4000k", "4 Мбит/с"], ["8000k", "8 Мбит/с"], ["10000k", "10 Мбит/с"], ["16000k", "16 Мбит/с"], ["20000k", "20 Мбит/с"], ["24000k", "24 Мбит/с"]])}</label>` + `<label>Битрейт${sel("bitrate", [["500k", "0,5 Мбит/с"], ["1000k", "1 Мбит/с"], ["2000k", "2 Мбит/с"], ["4000k", "4 Мбит/с"], ["8000k", "8 Мбит/с"], ["10000k", "10 Мбит/с"], ["16000k", "16 Мбит/с"], ["20000k", "20 Мбит/с"], ["24000k", "24 Мбит/с"]])}</label>` +
`<label>QP (квантизатор)<input type="number" data-f="qp" min="0" max="51" value="${pr.qp}" style="width:72px"></label>` + `<label>QP (квантизатор)<input type="number" data-f="qp" min="0" max="51" value="${pr.qp}"></label>` +
`<label>Интервал ключевых кадров${sel("keyint", [[1, "1 секунда"], [2, "2 секунды"], [5, "5 секунд"]])}</label>` + `<label>Интервал ключевых кадров${sel("keyint", [[1, "1 секунда"], [2, "2 секунды"], [5, "5 секунд"]])}</label>` +
`<label>Пресет (Preset)${sel("preset", [["ultrafast", "Ultrafast"], ["superfast", "Superfast"], ["veryfast", "Veryfast"], ["faster", "Faster"], ["fast", "Fast"], ["medium", "Medium"]])}</label>` `<label>Пресет (Preset)${sel("preset", [["ultrafast", "Ultrafast"], ["superfast", "Superfast"], ["veryfast", "Veryfast"], ["faster", "Faster"], ["fast", "Fast"], ["medium", "Medium"]])}</label>` +
`<label>Готовый профиль${qpresetSel}</label>`
); );
}, },
@@ -1493,32 +1493,28 @@ const Settings = {
const codec = row.querySelector('[data-f="codec"]'); const codec = row.querySelector('[data-f="codec"]');
const prof = row.querySelector('[data-f="profile"]'); const prof = row.querySelector('[data-f="profile"]');
const syncCodec = () => { prof.disabled = codec.value === "vp9"; }; const syncCodec = () => { prof.disabled = codec.value === "vp9"; };
syncCodec(); // QP ↔ битрейт: активен только параметр выбранного режима, второй — неактивен
// режим контроля битрейта: VBR/CBR → поле «Битрейт», QP → поле «QP»
const rcSel = row.querySelector('[data-f="rc"]'); const rcSel = row.querySelector('[data-f="rc"]');
const brLabel = row.querySelector('[data-f="bitrate"]').closest("label"); const brSel = row.querySelector('[data-f="bitrate"]');
const qpLabel = row.querySelector('[data-f="qp"]').closest("label"); const qpInp = row.querySelector('[data-f="qp"]');
const syncRc = () => { const isQp = rcSel.value === "qp"; qpLabel.hidden = !isQp; brLabel.hidden = isQp; }; const syncRc = () => { const isQp = rcSel.value === "qp"; brSel.disabled = isQp; qpInp.disabled = !isQp; };
syncRc(); // «Готовый профиль» (SOFT/MEDIUM/HARD): меню показывает совпадение или «— свой —»
// подсветить пресет, совпадающий с текущими значениями const qpreset = row.querySelector(".qpreset");
const highlight = () => { const syncQuality = () => {
const cur = { codec: codec.value, profile: prof.value, bitrate: row.querySelector('[data-f="bitrate"]').value, qpreset.value = matchPreset({ codec: codec.value, profile: prof.value, bitrate: brSel.value,
keyint: row.querySelector('[data-f="keyint"]').value, preset: row.querySelector('[data-f="preset"]').value }; keyint: row.querySelector('[data-f="keyint"]').value, preset: row.querySelector('[data-f="preset"]').value });
const name = matchPreset(cur);
row.querySelectorAll(".tr-preset").forEach((b) => b.classList.toggle("active", b.dataset.preset === name));
}; };
// общий пресет — выставить все параметры разом qpreset.onchange = () => {
row.querySelectorAll(".tr-preset").forEach((btn) => { const p = TR_PRESETS[qpreset.value];
btn.onclick = () => { if (!p) return;
const p = TR_PRESETS[btn.dataset.preset]; const put = (f, v) => { const s = row.querySelector(`[data-f="${f}"]`); if (s) s.value = String(v); };
const put = (f, v) => { const s = row.querySelector(`[data-f="${f}"]`); if (s) s.value = String(v); }; put("codec", p.codec); put("profile", p.profile); put("bitrate", p.bitrate);
put("codec", p.codec); put("profile", p.profile); put("bitrate", p.bitrate); put("keyint", p.keyint); put("preset", p.preset); put("rc", "vbr");
put("keyint", p.keyint); put("preset", p.preset); put("rc", "vbr"); syncCodec(); syncRc();
syncCodec(); syncRc(); highlight(); };
}; syncCodec(); syncRc();
});
row.querySelectorAll("select[data-f]").forEach((s) => { row.querySelectorAll("select[data-f]").forEach((s) => {
s.addEventListener("change", () => { syncCodec(); syncRc(); highlight(); }); s.addEventListener("change", () => { syncCodec(); syncRc(); syncQuality(); });
}); });
}); });
document.getElementById("prefs-save").onclick = () => { document.getElementById("prefs-save").onclick = () => {
+3 -1
View File
@@ -147,7 +147,7 @@ input[type="checkbox"]:checked {
} }
input[type="checkbox"]:checked::before { transform: translateX(18px); background: var(--ok); } input[type="checkbox"]:checked::before { transform: translateX(18px); background: var(--ok); }
input[type="checkbox"]:focus-visible { outline: 2px solid var(--ok); outline-offset: 2px; } input[type="checkbox"]:focus-visible { outline: 2px solid var(--ok); outline-offset: 2px; }
input[type="date"], select { input[type="date"], input[type="number"], select {
background: var(--panel-2); color: var(--text); border: 1px solid var(--border); background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
padding: 6px 8px; border-radius: 6px; padding: 6px 8px; border-radius: 6px;
} }
@@ -340,6 +340,8 @@ input[type="date"], select {
.tr-row[hidden] { display: none; } .tr-row[hidden] { display: none; }
.tr-row label { display: flex; flex-direction: column; gap: 5px; font-size: 12px; color: var(--text-dim); } .tr-row label { display: flex; flex-direction: column; gap: 5px; font-size: 12px; color: var(--text-dim); }
.tr-row select { display: block; } .tr-row select { display: block; }
.tr-row input[type="number"] { display: block; width: 80px; }
.tr-row select:disabled, .tr-row input:disabled { opacity: .4; cursor: not-allowed; }
/* общие пресеты перекодирования (SOFT / MEDIUM / HARD) */ /* общие пресеты перекодирования (SOFT / MEDIUM / HARD) */
.tr-presets { flex-basis: 100%; display: flex; align-items: center; gap: 8px; padding-left: 28px; } .tr-presets { flex-basis: 100%; display: flex; align-items: center; gap: 8px; padding-left: 28px; }
.tr-preset { .tr-preset {