This commit is contained in:
949036910@qq.com
2026-05-31 23:03:35 +08:00
parent 4120f37c94
commit b16053c46e
3 changed files with 127 additions and 63 deletions

View File

@@ -18,8 +18,10 @@ import { revealLocalFileInFolder } from "../../services/fileExport.js";
import { import {
createDefaultGlobalDisplay, createDefaultGlobalDisplay,
createDefaultMixCutSettings, createDefaultMixCutSettings,
deriveGlobalDisplayFromSettings,
MINIMIZED_POSITION_OPTIONS, MINIMIZED_POSITION_OPTIONS,
PIP_POSITION_OPTIONS, PIP_POSITION_OPTIONS,
PIP_SCALE_MODE_OPTIONS,
SELECTION_MODE_OPTIONS, SELECTION_MODE_OPTIONS,
} from "../../utils/mixCutDefaults.js"; } from "../../utils/mixCutDefaults.js";
import { formatRecordTimeRange } from "../../utils/srtParser.js"; import { formatRecordTimeRange } from "../../utils/srtParser.js";
@@ -52,12 +54,6 @@ const displayModeOptions = [
{ label: "画中画", value: "pip" }, { label: "画中画", value: "pip" },
]; ];
const scaleModeOptions = [
{ label: "适应", value: "fit" },
{ label: "填充", value: "fill" },
{ label: "拉伸", value: "stretch" },
];
const shapeOptions = [ const shapeOptions = [
{ label: "方形", value: "square" }, { label: "方形", value: "square" },
{ label: "圆形", value: "circle" }, { label: "圆形", value: "circle" },
@@ -101,10 +97,7 @@ function applyIncomingSettings(raw) {
settings.value = { ...createDefaultMixCutSettings(), ...raw }; settings.value = { ...createDefaultMixCutSettings(), ...raw };
records.value = Array.isArray(raw.records) ? [...raw.records] : []; records.value = Array.isArray(raw.records) ? [...raw.records] : [];
subtitleFilePath.value = raw.subtitleFilePath || ""; subtitleFilePath.value = raw.subtitleFilePath || "";
globalDisplay.value = { globalDisplay.value = deriveGlobalDisplayFromSettings(raw);
...createDefaultGlobalDisplay(),
displayMode: raw.displayMode === "pip" ? "pip" : "fullscreen",
};
} }
async function loadExistingSubtitleLines() { async function loadExistingSubtitleLines() {
@@ -185,7 +178,12 @@ function onPickLibraryForLine(lineIndex) {
async function onPickLocalForLine(lineIndex) { async function onPickLocalForLine(lineIndex) {
const path = await pickLocalMixCutMaterial(); const path = await pickLocalMixCutMaterial();
if (!path) return; if (!path) return;
bindMaterialToSubtitleLine(settings.value, lineIndex, path); bindMaterialToSubtitleLine(
settings.value,
lineIndex,
path,
globalDisplay.value,
);
showFeedback("success", "素材已选择"); showFeedback("success", "素材已选择");
} }
@@ -203,11 +201,21 @@ function onMaterialSelected(item) {
? Array.from(selectedLineIndices.value) ? Array.from(selectedLineIndices.value)
: [target.lineIndex]; : [target.lineIndex];
if (indices.length > 1) { if (indices.length > 1) {
bindMaterialToSubtitleLines(settings.value, indices, item.path); bindMaterialToSubtitleLines(
settings.value,
indices,
item.path,
globalDisplay.value,
);
showFeedback("success", "已为选中的多条字幕绑定同一个素材"); showFeedback("success", "已为选中的多条字幕绑定同一个素材");
selectedLineIndices.value = new Set(); selectedLineIndices.value = new Set();
} else { } else {
bindMaterialToSubtitleLine(settings.value, target.lineIndex, item.path); bindMaterialToSubtitleLine(
settings.value,
target.lineIndex,
item.path,
globalDisplay.value,
);
showFeedback("success", "素材已选择"); showFeedback("success", "素材已选择");
} }
} else if (target.type === "fixed-segment") { } else if (target.type === "fixed-segment") {
@@ -362,16 +370,19 @@ defineExpose({
/> />
</div> </div>
<div> <div>
<label class="mb-1 block text-xs text-slate-400"> <label class="mb-1 block text-xs text-slate-400">尺寸占比</label>
尺寸占比 {{ globalDisplay.originalVideoMinimized.sizePercent }}% <div class="flex items-center gap-2">
</label>
<Slider <Slider
v-model="globalDisplay.originalVideoMinimized.sizePercent" v-model="globalDisplay.originalVideoMinimized.sizePercent"
:min="10" :min="10"
:max="50" :max="50"
:step="5" :step="5"
class="w-full" class="min-w-0 flex-1"
/> />
<span class="w-10 shrink-0 text-right text-xs text-slate-400">
{{ globalDisplay.originalVideoMinimized.sizePercent }}%
</span>
</div>
</div> </div>
</template> </template>
</div> </div>
@@ -392,32 +403,36 @@ defineExpose({
/> />
</div> </div>
<div> <div>
<label class="mb-1 block text-xs text-slate-400"> <label class="mb-1 block text-xs text-slate-400">尺寸占比</label>
尺寸占比 {{ globalDisplay.pipSizePercent }}% <div class="flex items-center gap-2">
</label>
<Slider <Slider
v-model="globalDisplay.pipSizePercent" v-model="globalDisplay.pipSizePercent"
:min="10" :min="10"
:max="80" :max="80"
:step="5" :step="5"
class="w-full" class="min-w-0 flex-1"
/> />
<span class="w-10 shrink-0 text-right text-xs text-slate-400">
{{ globalDisplay.pipSizePercent }}%
</span>
</div>
</div> </div>
<div> <div>
<label class="mb-1 block text-xs text-slate-400">缩放模式</label> <label class="mb-1 block text-xs text-slate-400">缩放模式</label>
<SelectButton <Select
v-model="globalDisplay.pipScaleMode" v-model="globalDisplay.pipScaleMode"
:options="scaleModeOptions" :options="PIP_SCALE_MODE_OPTIONS"
option-label="label" option-label="label"
option-value="value" option-value="value"
size="small" size="small"
class="w-full"
/> />
</div> </div>
</div> </div>
</div> </div>
<p class="mt-3 text-xs text-slate-500"> <p class="mt-3 text-xs text-slate-500">
💡 提示修改上述设置后点击应用到所有素材按钮将统一应用到固定模式关键词模式和字幕模式的所有素材配置 💡 提示保存时将自动应用上述设置到所有素材也可点击应用到所有素材立即预览效果
</p> </p>
</div> </div>

View File

@@ -4,7 +4,9 @@ import { open } from "@tauri-apps/plugin-dialog";
import { listMaterials } from "./materialDb.js"; import { listMaterials } from "./materialDb.js";
import { import {
createDefaultGlobalDisplay,
createDefaultMixCutSettings, createDefaultMixCutSettings,
createDefaultOriginalVideoMinimized,
normalizeMixCutSettings, normalizeMixCutSettings,
} from "../utils/mixCutDefaults.js"; } from "../utils/mixCutDefaults.js";
import { parseSrtToRecords } from "../utils/srtParser.js"; import { parseSrtToRecords } from "../utils/srtParser.js";
@@ -346,18 +348,25 @@ export async function listMixCutMaterialLibrary() {
} }
/** /**
* @param {object} settings
* @param {object} globalDisplay * @param {object} globalDisplay
*/ */
export function applyGlobalDisplayToAllMaterials(settings, globalDisplay) { export function createMaterialDisplayPatch(globalDisplay) {
const g = globalDisplay; const g = globalDisplay || createDefaultGlobalDisplay();
const patch = { return {
displayMode: g.displayMode, displayMode: g.displayMode,
pipPosition: g.pipPosition, pipPosition: g.pipPosition,
pipSizePercent: g.pipSizePercent, pipSizePercent: g.pipSizePercent,
pipScaleMode: g.pipScaleMode, pipScaleMode: g.pipScaleMode,
originalVideoMinimized: { ...g.originalVideoMinimized }, originalVideoMinimized: { ...(g.originalVideoMinimized || createDefaultOriginalVideoMinimized()) },
}; };
}
/**
* @param {object} settings
* @param {object} globalDisplay
*/
export function applyGlobalDisplayToAllMaterials(settings, globalDisplay) {
const patch = createMaterialDisplayPatch(globalDisplay);
["opening", "middle", "ending"].forEach((key) => { ["opening", "middle", "ending"].forEach((key) => {
Object.assign(settings.fixedModeConfig[key], patch); Object.assign(settings.fixedModeConfig[key], patch);
@@ -389,8 +398,14 @@ export function findMappingForLine(settings, lineIndex) {
* @param {object} settings * @param {object} settings
* @param {number} lineIndex * @param {number} lineIndex
* @param {string} materialPath * @param {string} materialPath
* @param {object} [globalDisplay]
*/ */
export function bindMaterialToSubtitleLine(settings, lineIndex, materialPath) { export function bindMaterialToSubtitleLine(
settings,
lineIndex,
materialPath,
globalDisplay,
) {
const mappings = settings.subtitleModeConfig.mappings; const mappings = settings.subtitleModeConfig.mappings;
const existing = findMappingForLine(settings, lineIndex); const existing = findMappingForLine(settings, lineIndex);
const isImage = isImageMaterialPath(materialPath); const isImage = isImageMaterialPath(materialPath);
@@ -410,16 +425,7 @@ export function bindMaterialToSubtitleLine(settings, lineIndex, materialPath) {
materialPath, materialPath,
isImage, isImage,
selectionMode: "specified", selectionMode: "specified",
displayMode: settings.displayMode || "fullscreen", ...createMaterialDisplayPatch(globalDisplay),
pipPosition: "bottom-right",
pipSizePercent: 30,
pipScaleMode: "fit",
originalVideoMinimized: {
enabled: false,
shape: "square",
position: "bottom-right",
sizePercent: 25,
},
}; };
filtered.push(created); filtered.push(created);
settings.subtitleModeConfig.mappings = filtered; settings.subtitleModeConfig.mappings = filtered;
@@ -430,8 +436,14 @@ export function bindMaterialToSubtitleLine(settings, lineIndex, materialPath) {
* @param {object} settings * @param {object} settings
* @param {number[]} lineIndices * @param {number[]} lineIndices
* @param {string} materialPath * @param {string} materialPath
* @param {object} [globalDisplay]
*/ */
export function bindMaterialToSubtitleLines(settings, lineIndices, materialPath) { export function bindMaterialToSubtitleLines(
settings,
lineIndices,
materialPath,
globalDisplay,
) {
const indices = [...new Set(lineIndices)].sort((a, b) => a - b); const indices = [...new Set(lineIndices)].sort((a, b) => a - b);
if (!indices.length) return; if (!indices.length) return;
@@ -445,16 +457,7 @@ export function bindMaterialToSubtitleLines(settings, lineIndices, materialPath)
materialPath, materialPath,
isImage, isImage,
selectionMode: "specified", selectionMode: "specified",
displayMode: settings.displayMode || "fullscreen", ...createMaterialDisplayPatch(globalDisplay),
pipPosition: "bottom-right",
pipSizePercent: 30,
pipScaleMode: "fit",
originalVideoMinimized: {
enabled: false,
shape: "square",
position: "bottom-right",
sizePercent: 25,
},
}); });
} }
@@ -472,6 +475,7 @@ export function removeMaterialFromSubtitleLine(settings, lineIndex) {
* @param {object} settings * @param {object} settings
*/ */
export function prepareMixCutSettingsForSave(settings, globalDisplay) { export function prepareMixCutSettingsForSave(settings, globalDisplay) {
applyGlobalDisplayToAllMaterials(settings, globalDisplay);
const next = normalizeMixCutSettings(settings); const next = normalizeMixCutSettings(settings);
next.fixedModeConfig.enabled = next.mode === "fixed"; next.fixedModeConfig.enabled = next.mode === "fixed";
next.keywordModeConfig.enabled = next.mode === "keyword"; next.keywordModeConfig.enabled = next.mode === "keyword";

View File

@@ -198,6 +198,51 @@ export const PIP_POSITION_OPTIONS = [
{ label: "右下角", value: "bottom-right" }, { label: "右下角", value: "bottom-right" },
]; ];
export const PIP_SCALE_MODE_OPTIONS = [
{ label: "保持比例+黑边", value: "fit" },
{ label: "保持比例+裁剪", value: "fill" },
{ label: "强制拉伸", value: "stretch" },
];
/**
* 从已保存的混剪配置推断全局显示设置(对齐 Electron 加载后 ue 的展示)
* @param {object | null | undefined} settings
*/
export function deriveGlobalDisplayFromSettings(settings) {
const base = createDefaultGlobalDisplay();
if (!settings || typeof settings !== "object") return base;
const candidates = [
settings.fixedModeConfig?.opening,
...(Array.isArray(settings.keywordModeConfig?.groups)
? settings.keywordModeConfig.groups
: []),
...(Array.isArray(settings.subtitleModeConfig?.mappings)
? settings.subtitleModeConfig.mappings
: []),
].filter(Boolean);
const source = candidates.find((c) => c.displayMode) || candidates[0];
const displayMode =
source?.displayMode ||
(settings.displayMode === "pip" ? "pip" : "fullscreen");
if (!source) {
return { ...base, displayMode };
}
return {
displayMode,
pipPosition: source.pipPosition || base.pipPosition,
pipSizePercent: source.pipSizePercent ?? base.pipSizePercent,
pipScaleMode: source.pipScaleMode || base.pipScaleMode,
originalVideoMinimized: {
...base.originalVideoMinimized,
...(source.originalVideoMinimized || {}),
},
};
}
export const MINIMIZED_POSITION_OPTIONS = [ export const MINIMIZED_POSITION_OPTIONS = [
{ label: "左上角", value: "top-left" }, { label: "左上角", value: "top-left" },
{ label: "右上角", value: "top-right" }, { label: "右上角", value: "top-right" },