90e1460a1b
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m6s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 38s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m5s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m48s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 3m8s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m43s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m16s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 58s
CI/CD Pipeline / Integration Tests (push) Successful in 4m39s
CI/CD Pipeline / Validate - Style (push) Successful in 5m20s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m43s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m49s
AI Code Review / AI Code Review (pull_request) Successful in 6m58s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m14s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m50s
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 10m31s
CI/CD Pipeline / CI Gate (pull_request) Failing after 1s
CI/CD Pipeline / Unit Tests (push) Successful in 11m34s
CI/CD Pipeline / Validate - Security (push) Successful in 20m18s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
216 lines
6.8 KiB
TypeScript
216 lines
6.8 KiB
TypeScript
import { useCallback, useMemo } from "react"
|
||
import { TITLE_PRESETS } from "../../constants"
|
||
import { TITLE_PRESETS as NEW_TITLE_PRESETS } from "@/components/title/constants"
|
||
import { titleStyleConfigToCamel } from "@/components/title/utils"
|
||
import type { TitleSettings } from "../../types"
|
||
|
||
interface UseTitleStyleUpdatersOptions {
|
||
titleSettings: TitleSettings
|
||
onTitleSettingsChange: (settings: TitleSettings) => void
|
||
}
|
||
|
||
/**
|
||
* 标题样式更新 Hook
|
||
* 封装标题文字、位置、字体、样式等所有设置更新函数
|
||
*/
|
||
export function useTitleStyleUpdaters({
|
||
titleSettings,
|
||
onTitleSettingsChange,
|
||
}: UseTitleStyleUpdatersOptions) {
|
||
/** 匹配预设:对新预设(snake→camel 后)全字段比;旧预设只比 color/bold/italic/stroke/shadow */
|
||
const getActivePreset = (settings: TitleSettings): string | null => {
|
||
// 新预设匹配:font/size/color/bold/stroke/shadow/bg 全部对齐才算命中
|
||
for (const p of NEW_TITLE_PRESETS) {
|
||
const camel = titleStyleConfigToCamel(p.style)
|
||
if (
|
||
(camel.font ?? null) === (settings.font ?? null) &&
|
||
(camel.color ?? null) === (settings.color ?? null) &&
|
||
(camel.bold ?? null) === (settings.bold ?? null) &&
|
||
(camel.italic ?? null) === (settings.italic ?? null) &&
|
||
(camel.stroke ?? null) === (settings.stroke ?? null) &&
|
||
(camel.shadow ?? null) === (settings.shadow ?? null) &&
|
||
(camel.bgEnabled ?? null) === (settings.bgEnabled ?? null)
|
||
) {
|
||
return p.key
|
||
}
|
||
}
|
||
// fallback 旧预设(legacy)
|
||
for (const p of TITLE_PRESETS) {
|
||
if (
|
||
settings.color === p.style.color &&
|
||
settings.bold === p.style.bold &&
|
||
settings.italic === p.style.italic &&
|
||
settings.stroke === p.style.stroke &&
|
||
settings.shadow === p.style.shadow
|
||
) {
|
||
return p.key
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
const activePreset = useMemo(() => getActivePreset(titleSettings), [titleSettings])
|
||
|
||
const updateTitle = useCallback(
|
||
(title: string) => {
|
||
onTitleSettingsChange({ ...titleSettings, title })
|
||
},
|
||
[titleSettings, onTitleSettingsChange],
|
||
)
|
||
|
||
const toggleAiAutoSelect = useCallback(() => {
|
||
onTitleSettingsChange({ ...titleSettings, aiAutoSelect: !titleSettings.aiAutoSelect })
|
||
}, [titleSettings, onTitleSettingsChange])
|
||
|
||
const updatePosition = useCallback(
|
||
(position: string) => {
|
||
// 切回三档预设时清掉自定义坐标
|
||
onTitleSettingsChange({ ...titleSettings, position, posX: null, posY: null })
|
||
},
|
||
[titleSettings, onTitleSettingsChange],
|
||
)
|
||
|
||
/** 拖拽更新自定义位置(由预览播放器调用) */
|
||
const updateTitlePosition = useCallback(
|
||
(posX: number, posY: number) => {
|
||
onTitleSettingsChange({ ...titleSettings, position: "custom", posX, posY })
|
||
},
|
||
[titleSettings, onTitleSettingsChange],
|
||
)
|
||
|
||
const updateFont = useCallback(
|
||
(font: string) => {
|
||
onTitleSettingsChange({ ...titleSettings, font })
|
||
},
|
||
[titleSettings, onTitleSettingsChange],
|
||
)
|
||
|
||
const updateSize = useCallback(
|
||
(size: number) => {
|
||
onTitleSettingsChange({ ...titleSettings, size })
|
||
},
|
||
[titleSettings, onTitleSettingsChange],
|
||
)
|
||
|
||
const updateColor = useCallback(
|
||
(color: string) => {
|
||
onTitleSettingsChange({ ...titleSettings, color })
|
||
},
|
||
[titleSettings, onTitleSettingsChange],
|
||
)
|
||
|
||
const toggleBold = useCallback(() => {
|
||
onTitleSettingsChange({ ...titleSettings, bold: !titleSettings.bold })
|
||
}, [titleSettings, onTitleSettingsChange])
|
||
|
||
const toggleItalic = useCallback(() => {
|
||
onTitleSettingsChange({ ...titleSettings, italic: !titleSettings.italic })
|
||
}, [titleSettings, onTitleSettingsChange])
|
||
|
||
const toggleStroke = useCallback(() => {
|
||
onTitleSettingsChange({
|
||
...titleSettings,
|
||
stroke: !titleSettings.stroke,
|
||
// 开启描边时若宽度过小给个默认值(让滑块可见可调)
|
||
strokeWidth:
|
||
!titleSettings.stroke && (titleSettings.strokeWidth ?? 0) < 2
|
||
? 4
|
||
: titleSettings.strokeWidth,
|
||
})
|
||
}, [titleSettings, onTitleSettingsChange])
|
||
|
||
const toggleShadow = useCallback(() => {
|
||
onTitleSettingsChange({ ...titleSettings, shadow: !titleSettings.shadow })
|
||
}, [titleSettings, onTitleSettingsChange])
|
||
|
||
/** 应用预设:正确把 snake_case 的 preset.style 转为 camelCase 再 spread */
|
||
const applyPreset = useCallback(
|
||
(presetKey: string) => {
|
||
const newPreset = NEW_TITLE_PRESETS.find((p) => p.key === presetKey)
|
||
const oldPreset = TITLE_PRESETS.find((p) => p.key === presetKey)
|
||
if (newPreset) {
|
||
onTitleSettingsChange({
|
||
...titleSettings,
|
||
...titleStyleConfigToCamel(newPreset.style),
|
||
// 封面独立标题保持不变(不清空,避免破坏封面定制)
|
||
lineOverrides: [],
|
||
})
|
||
return
|
||
}
|
||
if (!oldPreset) return
|
||
onTitleSettingsChange({
|
||
...titleSettings,
|
||
color: oldPreset.style.color as string,
|
||
bold: oldPreset.style.bold as boolean,
|
||
italic: oldPreset.style.italic as boolean,
|
||
stroke: oldPreset.style.stroke as boolean,
|
||
shadow: oldPreset.style.shadow as boolean,
|
||
})
|
||
},
|
||
[titleSettings, onTitleSettingsChange],
|
||
)
|
||
|
||
/** 应用模板:用模板(camelCase TitleSettings)覆盖样式字段,保留 title/aiAutoSelect */
|
||
const applyTemplate = useCallback(
|
||
(tpl: TitleSettings) => {
|
||
onTitleSettingsChange({
|
||
...titleSettings,
|
||
// 覆盖样式字段
|
||
position: tpl.position,
|
||
posX: tpl.posX,
|
||
posY: tpl.posY,
|
||
font: tpl.font,
|
||
size: tpl.size,
|
||
bold: tpl.bold,
|
||
italic: tpl.italic,
|
||
stroke: tpl.stroke,
|
||
shadow: tpl.shadow,
|
||
color: tpl.color,
|
||
lineHeight: tpl.lineHeight,
|
||
marginTop: tpl.marginTop,
|
||
maxCharsPerLine: tpl.maxCharsPerLine,
|
||
strokeWidth: tpl.strokeWidth,
|
||
strokeColor: tpl.strokeColor,
|
||
shadowOffsetX: tpl.shadowOffsetX,
|
||
shadowOffsetY: tpl.shadowOffsetY,
|
||
shadowBlur: tpl.shadowBlur,
|
||
shadowColor: tpl.shadowColor,
|
||
bgEnabled: tpl.bgEnabled,
|
||
bgColor: tpl.bgColor,
|
||
bgPadding: tpl.bgPadding,
|
||
bgRadius: tpl.bgRadius,
|
||
lineOverrides: [],
|
||
// coverTitle 保留用户当前值,不强制覆盖
|
||
})
|
||
},
|
||
[titleSettings, onTitleSettingsChange],
|
||
)
|
||
|
||
/** 通用字段更新(patch) */
|
||
const updateStyle = useCallback(
|
||
(patch: Partial<TitleSettings>) => {
|
||
onTitleSettingsChange({ ...titleSettings, ...patch })
|
||
},
|
||
[titleSettings, onTitleSettingsChange],
|
||
)
|
||
|
||
return {
|
||
activePreset,
|
||
titlePresets: NEW_TITLE_PRESETS,
|
||
updateTitle,
|
||
toggleAiAutoSelect,
|
||
updatePosition,
|
||
updateTitlePosition,
|
||
updateFont,
|
||
updateSize,
|
||
updateColor,
|
||
toggleBold,
|
||
toggleItalic,
|
||
toggleStroke,
|
||
toggleShadow,
|
||
applyPreset,
|
||
applyTemplate,
|
||
updateStyle,
|
||
}
|
||
}
|