111
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user