111
This commit is contained in:
@@ -18,8 +18,10 @@ import { revealLocalFileInFolder } from "../../services/fileExport.js";
|
||||
import {
|
||||
createDefaultGlobalDisplay,
|
||||
createDefaultMixCutSettings,
|
||||
deriveGlobalDisplayFromSettings,
|
||||
MINIMIZED_POSITION_OPTIONS,
|
||||
PIP_POSITION_OPTIONS,
|
||||
PIP_SCALE_MODE_OPTIONS,
|
||||
SELECTION_MODE_OPTIONS,
|
||||
} from "../../utils/mixCutDefaults.js";
|
||||
import { formatRecordTimeRange } from "../../utils/srtParser.js";
|
||||
@@ -52,12 +54,6 @@ const displayModeOptions = [
|
||||
{ label: "画中画", value: "pip" },
|
||||
];
|
||||
|
||||
const scaleModeOptions = [
|
||||
{ label: "适应", value: "fit" },
|
||||
{ label: "填充", value: "fill" },
|
||||
{ label: "拉伸", value: "stretch" },
|
||||
];
|
||||
|
||||
const shapeOptions = [
|
||||
{ label: "方形", value: "square" },
|
||||
{ label: "圆形", value: "circle" },
|
||||
@@ -101,10 +97,7 @@ function applyIncomingSettings(raw) {
|
||||
settings.value = { ...createDefaultMixCutSettings(), ...raw };
|
||||
records.value = Array.isArray(raw.records) ? [...raw.records] : [];
|
||||
subtitleFilePath.value = raw.subtitleFilePath || "";
|
||||
globalDisplay.value = {
|
||||
...createDefaultGlobalDisplay(),
|
||||
displayMode: raw.displayMode === "pip" ? "pip" : "fullscreen",
|
||||
};
|
||||
globalDisplay.value = deriveGlobalDisplayFromSettings(raw);
|
||||
}
|
||||
|
||||
async function loadExistingSubtitleLines() {
|
||||
@@ -185,7 +178,12 @@ function onPickLibraryForLine(lineIndex) {
|
||||
async function onPickLocalForLine(lineIndex) {
|
||||
const path = await pickLocalMixCutMaterial();
|
||||
if (!path) return;
|
||||
bindMaterialToSubtitleLine(settings.value, lineIndex, path);
|
||||
bindMaterialToSubtitleLine(
|
||||
settings.value,
|
||||
lineIndex,
|
||||
path,
|
||||
globalDisplay.value,
|
||||
);
|
||||
showFeedback("success", "素材已选择");
|
||||
}
|
||||
|
||||
@@ -203,11 +201,21 @@ function onMaterialSelected(item) {
|
||||
? Array.from(selectedLineIndices.value)
|
||||
: [target.lineIndex];
|
||||
if (indices.length > 1) {
|
||||
bindMaterialToSubtitleLines(settings.value, indices, item.path);
|
||||
bindMaterialToSubtitleLines(
|
||||
settings.value,
|
||||
indices,
|
||||
item.path,
|
||||
globalDisplay.value,
|
||||
);
|
||||
showFeedback("success", "已为选中的多条字幕绑定同一个素材");
|
||||
selectedLineIndices.value = new Set();
|
||||
} else {
|
||||
bindMaterialToSubtitleLine(settings.value, target.lineIndex, item.path);
|
||||
bindMaterialToSubtitleLine(
|
||||
settings.value,
|
||||
target.lineIndex,
|
||||
item.path,
|
||||
globalDisplay.value,
|
||||
);
|
||||
showFeedback("success", "素材已选择");
|
||||
}
|
||||
} else if (target.type === "fixed-segment") {
|
||||
@@ -362,16 +370,19 @@ defineExpose({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs text-slate-400">
|
||||
尺寸占比 {{ globalDisplay.originalVideoMinimized.sizePercent }}%
|
||||
</label>
|
||||
<Slider
|
||||
v-model="globalDisplay.originalVideoMinimized.sizePercent"
|
||||
:min="10"
|
||||
:max="50"
|
||||
:step="5"
|
||||
class="w-full"
|
||||
/>
|
||||
<label class="mb-1 block text-xs text-slate-400">尺寸占比</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Slider
|
||||
v-model="globalDisplay.originalVideoMinimized.sizePercent"
|
||||
:min="10"
|
||||
:max="50"
|
||||
:step="5"
|
||||
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>
|
||||
</template>
|
||||
</div>
|
||||
@@ -392,32 +403,36 @@ defineExpose({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs text-slate-400">
|
||||
尺寸占比 {{ globalDisplay.pipSizePercent }}%
|
||||
</label>
|
||||
<Slider
|
||||
v-model="globalDisplay.pipSizePercent"
|
||||
:min="10"
|
||||
:max="80"
|
||||
:step="5"
|
||||
class="w-full"
|
||||
/>
|
||||
<label class="mb-1 block text-xs text-slate-400">尺寸占比</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Slider
|
||||
v-model="globalDisplay.pipSizePercent"
|
||||
:min="10"
|
||||
:max="80"
|
||||
:step="5"
|
||||
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>
|
||||
<label class="mb-1 block text-xs text-slate-400">缩放模式</label>
|
||||
<SelectButton
|
||||
<Select
|
||||
v-model="globalDisplay.pipScaleMode"
|
||||
:options="scaleModeOptions"
|
||||
:options="PIP_SCALE_MODE_OPTIONS"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
size="small"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-3 text-xs text-slate-500">
|
||||
💡 提示:修改上述设置后,点击「应用到所有素材」按钮,将统一应用到固定模式、关键词模式和字幕模式的所有素材配置
|
||||
💡 提示:保存时将自动应用上述设置到所有素材;也可点击「应用到所有素材」立即预览效果
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import { open } from "@tauri-apps/plugin-dialog";
|
||||
|
||||
import { listMaterials } from "./materialDb.js";
|
||||
import {
|
||||
createDefaultGlobalDisplay,
|
||||
createDefaultMixCutSettings,
|
||||
createDefaultOriginalVideoMinimized,
|
||||
normalizeMixCutSettings,
|
||||
} from "../utils/mixCutDefaults.js";
|
||||
import { parseSrtToRecords } from "../utils/srtParser.js";
|
||||
@@ -346,18 +348,25 @@ export async function listMixCutMaterialLibrary() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} settings
|
||||
* @param {object} globalDisplay
|
||||
*/
|
||||
export function applyGlobalDisplayToAllMaterials(settings, globalDisplay) {
|
||||
const g = globalDisplay;
|
||||
const patch = {
|
||||
export function createMaterialDisplayPatch(globalDisplay) {
|
||||
const g = globalDisplay || createDefaultGlobalDisplay();
|
||||
return {
|
||||
displayMode: g.displayMode,
|
||||
pipPosition: g.pipPosition,
|
||||
pipSizePercent: g.pipSizePercent,
|
||||
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) => {
|
||||
Object.assign(settings.fixedModeConfig[key], patch);
|
||||
@@ -389,8 +398,14 @@ export function findMappingForLine(settings, lineIndex) {
|
||||
* @param {object} settings
|
||||
* @param {number} lineIndex
|
||||
* @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 existing = findMappingForLine(settings, lineIndex);
|
||||
const isImage = isImageMaterialPath(materialPath);
|
||||
@@ -410,16 +425,7 @@ export function bindMaterialToSubtitleLine(settings, lineIndex, materialPath) {
|
||||
materialPath,
|
||||
isImage,
|
||||
selectionMode: "specified",
|
||||
displayMode: settings.displayMode || "fullscreen",
|
||||
pipPosition: "bottom-right",
|
||||
pipSizePercent: 30,
|
||||
pipScaleMode: "fit",
|
||||
originalVideoMinimized: {
|
||||
enabled: false,
|
||||
shape: "square",
|
||||
position: "bottom-right",
|
||||
sizePercent: 25,
|
||||
},
|
||||
...createMaterialDisplayPatch(globalDisplay),
|
||||
};
|
||||
filtered.push(created);
|
||||
settings.subtitleModeConfig.mappings = filtered;
|
||||
@@ -430,8 +436,14 @@ export function bindMaterialToSubtitleLine(settings, lineIndex, materialPath) {
|
||||
* @param {object} settings
|
||||
* @param {number[]} lineIndices
|
||||
* @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);
|
||||
if (!indices.length) return;
|
||||
|
||||
@@ -445,16 +457,7 @@ export function bindMaterialToSubtitleLines(settings, lineIndices, materialPath)
|
||||
materialPath,
|
||||
isImage,
|
||||
selectionMode: "specified",
|
||||
displayMode: settings.displayMode || "fullscreen",
|
||||
pipPosition: "bottom-right",
|
||||
pipSizePercent: 30,
|
||||
pipScaleMode: "fit",
|
||||
originalVideoMinimized: {
|
||||
enabled: false,
|
||||
shape: "square",
|
||||
position: "bottom-right",
|
||||
sizePercent: 25,
|
||||
},
|
||||
...createMaterialDisplayPatch(globalDisplay),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -472,6 +475,7 @@ export function removeMaterialFromSubtitleLine(settings, lineIndex) {
|
||||
* @param {object} settings
|
||||
*/
|
||||
export function prepareMixCutSettingsForSave(settings, globalDisplay) {
|
||||
applyGlobalDisplayToAllMaterials(settings, globalDisplay);
|
||||
const next = normalizeMixCutSettings(settings);
|
||||
next.fixedModeConfig.enabled = next.mode === "fixed";
|
||||
next.keywordModeConfig.enabled = next.mode === "keyword";
|
||||
|
||||
@@ -198,6 +198,51 @@ export const PIP_POSITION_OPTIONS = [
|
||||
{ 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 = [
|
||||
{ label: "左上角", value: "top-left" },
|
||||
{ label: "右上角", value: "top-right" },
|
||||
|
||||
Reference in New Issue
Block a user