refactor: 替换硬编码值为 DesignTokens + 添加触觉反馈和动画
硬编码值替换: - spacing: 20/24/16/12/8/4 → DesignTokens.Spacing.xl/xxl/lg/md/sm/xs - cornerRadius: 16/8/4 → DesignTokens.Radius.lg/sm/xs - padding(.top, 8) / padding(.leading, 4) → DesignTokens.Spacing.sm/xs 生成按钮替换: - 手动实现的渐变按钮替换为 SoftPrimaryButton 组件 - 添加底部安全间距 padding(.bottom, DesignTokens.Spacing.sm) 触觉反馈(iOS 17+ sensoryFeedback): - 比例选择切换:.selection - 生成按钮点击:.impact(weight: .medium) - 裁剪手势结束:.impact(weight: .light) 动画优化: - AspectRatioButton 添加选中状态过渡动画 - CropOverlay 添加比例切换平滑过渡动画 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,9 @@ struct EditorView: View {
|
||||
// 视频诊断
|
||||
@State private var videoDiagnosis: VideoDiagnosis?
|
||||
|
||||
// 触觉反馈触发
|
||||
@State private var generateTapCount: Int = 0
|
||||
|
||||
/// 是否使用 iPad 分栏布局(regular 宽度 + 横屏)
|
||||
private var useIPadLayout: Bool {
|
||||
horizontalSizeClass == .regular
|
||||
@@ -67,13 +70,16 @@ struct EditorView: View {
|
||||
.onDisappear {
|
||||
player?.pause()
|
||||
}
|
||||
.sensoryFeedback(.selection, trigger: selectedAspectRatio)
|
||||
.sensoryFeedback(.impact(weight: .medium), trigger: generateTapCount)
|
||||
.sensoryFeedback(.impact(weight: .light), trigger: lastCropScale)
|
||||
}
|
||||
|
||||
// MARK: - iPhone 布局(单列滚动)
|
||||
@ViewBuilder
|
||||
private var iPhoneLayout: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
VStack(spacing: DesignTokens.Spacing.xl) {
|
||||
cropPreview(height: 360)
|
||||
|
||||
if let diagnosis = videoDiagnosis, !diagnosis.suggestions.isEmpty {
|
||||
@@ -96,9 +102,9 @@ struct EditorView: View {
|
||||
// MARK: - iPad 布局(左右分栏)
|
||||
@ViewBuilder
|
||||
private var iPadLayout: some View {
|
||||
HStack(alignment: .top, spacing: 24) {
|
||||
HStack(alignment: .top, spacing: DesignTokens.Spacing.xxl) {
|
||||
// 左侧:视频预览
|
||||
VStack(spacing: 16) {
|
||||
VStack(spacing: DesignTokens.Spacing.lg) {
|
||||
cropPreview(height: 500, dynamicHeight: true)
|
||||
|
||||
if let diagnosis = videoDiagnosis, !diagnosis.suggestions.isEmpty {
|
||||
@@ -111,7 +117,7 @@ struct EditorView: View {
|
||||
|
||||
// 右侧:参数控制
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
VStack(spacing: DesignTokens.Spacing.lg) {
|
||||
coverFrameSection
|
||||
durationSection
|
||||
keyFrameSection
|
||||
@@ -178,8 +184,8 @@ struct EditorView: View {
|
||||
}
|
||||
}
|
||||
.frame(width: containerWidth, height: containerHeight)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.background(Color.black.clipShape(RoundedRectangle(cornerRadius: 16)))
|
||||
.clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.lg))
|
||||
.background(Color.black.clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.lg)))
|
||||
}
|
||||
.frame(height: height)
|
||||
}
|
||||
@@ -187,7 +193,7 @@ struct EditorView: View {
|
||||
// MARK: - 比例模板选择
|
||||
@ViewBuilder
|
||||
private var aspectRatioSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
|
||||
HStack {
|
||||
Image(systemName: "aspectratio")
|
||||
.foregroundStyle(.tint)
|
||||
@@ -195,7 +201,7 @@ struct EditorView: View {
|
||||
.font(.headline)
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
HStack(spacing: DesignTokens.Spacing.sm) {
|
||||
ForEach(AspectRatioTemplate.allCases, id: \.self) { template in
|
||||
AspectRatioButton(
|
||||
template: template,
|
||||
@@ -221,7 +227,7 @@ struct EditorView: View {
|
||||
// MARK: - 封面帧预览
|
||||
@ViewBuilder
|
||||
private var coverFrameSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
|
||||
HStack {
|
||||
Image(systemName: "photo")
|
||||
.foregroundStyle(.tint)
|
||||
@@ -234,15 +240,15 @@ struct EditorView: View {
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: DesignTokens.Spacing.md) {
|
||||
if let coverImage {
|
||||
Image(uiImage: coverImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 80, height: 120)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.sm))
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
RoundedRectangle(cornerRadius: DesignTokens.Radius.sm)
|
||||
.fill(Color.softPressed)
|
||||
.frame(width: 80, height: 120)
|
||||
.overlay {
|
||||
@@ -251,7 +257,7 @@ struct EditorView: View {
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
VStack(alignment: .leading, spacing: DesignTokens.Spacing.xs) {
|
||||
Text(String(localized: "editor.coverFrameHint1"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.textSecondary)
|
||||
@@ -269,7 +275,7 @@ struct EditorView: View {
|
||||
// MARK: - 时长控制
|
||||
@ViewBuilder
|
||||
private var durationSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
|
||||
HStack {
|
||||
Image(systemName: "timer")
|
||||
.foregroundStyle(.tint)
|
||||
@@ -306,7 +312,7 @@ struct EditorView: View {
|
||||
// MARK: - 封面帧时间选择
|
||||
@ViewBuilder
|
||||
private var keyFrameSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
|
||||
HStack {
|
||||
Image(systemName: "clock")
|
||||
.foregroundStyle(.tint)
|
||||
@@ -344,7 +350,7 @@ struct EditorView: View {
|
||||
// MARK: - AI 超分辨率开关
|
||||
@ViewBuilder
|
||||
private var aiEnhanceSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
|
||||
Toggle(isOn: $aiEnhanceEnabled) {
|
||||
HStack {
|
||||
Image(systemName: "wand.and.stars.inverse")
|
||||
@@ -368,8 +374,8 @@ struct EditorView: View {
|
||||
|
||||
// 模型下载进度
|
||||
if aiModelDownloading {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 8) {
|
||||
VStack(alignment: .leading, spacing: DesignTokens.Spacing.sm) {
|
||||
HStack(spacing: DesignTokens.Spacing.sm) {
|
||||
ProgressView()
|
||||
.scaleEffect(0.8)
|
||||
Text(String(localized: "editor.aiModelDownloading"))
|
||||
@@ -384,13 +390,13 @@ struct EditorView: View {
|
||||
.font(.caption2)
|
||||
.foregroundColor(.textSecondary)
|
||||
}
|
||||
.padding(.leading, 4)
|
||||
.padding(.leading, DesignTokens.Spacing.xs)
|
||||
}
|
||||
|
||||
if aiEnhanceEnabled && !aiModelDownloading {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
if aiModelNeedsDownload {
|
||||
HStack(spacing: 4) {
|
||||
HStack(spacing: DesignTokens.Spacing.xs) {
|
||||
Image(systemName: "arrow.down.circle")
|
||||
.foregroundStyle(.orange)
|
||||
.font(.caption)
|
||||
@@ -398,21 +404,21 @@ struct EditorView: View {
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
HStack(spacing: DesignTokens.Spacing.xs) {
|
||||
Image(systemName: "sparkles")
|
||||
.foregroundStyle(Color.accentPurple)
|
||||
.font(.caption)
|
||||
Text(String(localized: "editor.aiResolutionBoost"))
|
||||
.font(.caption)
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
HStack(spacing: DesignTokens.Spacing.xs) {
|
||||
Image(systemName: "clock")
|
||||
.foregroundStyle(Color.accentPurple)
|
||||
.font(.caption)
|
||||
Text(String(localized: "editor.aiProcessingTime"))
|
||||
.font(.caption)
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
HStack(spacing: DesignTokens.Spacing.xs) {
|
||||
Image(systemName: "cpu")
|
||||
.foregroundStyle(Color.accentPurple)
|
||||
.font(.caption)
|
||||
@@ -421,11 +427,11 @@ struct EditorView: View {
|
||||
}
|
||||
}
|
||||
.foregroundColor(.textSecondary)
|
||||
.padding(.leading, 4)
|
||||
.padding(.leading, DesignTokens.Spacing.xs)
|
||||
}
|
||||
|
||||
if !AIEnhancer.isAvailable() {
|
||||
HStack(spacing: 4) {
|
||||
HStack(spacing: DesignTokens.Spacing.xs) {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.foregroundStyle(.yellow)
|
||||
.font(.caption)
|
||||
@@ -433,7 +439,7 @@ struct EditorView: View {
|
||||
.font(.caption)
|
||||
.foregroundColor(.textSecondary)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
.padding(.top, DesignTokens.Spacing.xs)
|
||||
}
|
||||
}
|
||||
.padding(DesignTokens.Spacing.lg)
|
||||
@@ -448,7 +454,7 @@ struct EditorView: View {
|
||||
// MARK: - 兼容模式开关
|
||||
@ViewBuilder
|
||||
private var compatibilitySection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
|
||||
Toggle(isOn: $compatibilityMode) {
|
||||
HStack {
|
||||
Image(systemName: "gearshape.2")
|
||||
@@ -466,28 +472,28 @@ struct EditorView: View {
|
||||
|
||||
if compatibilityMode {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 4) {
|
||||
HStack(spacing: DesignTokens.Spacing.xs) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
.font(.caption)
|
||||
Text(String(localized: "editor.resolution720p"))
|
||||
.font(.caption)
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
HStack(spacing: DesignTokens.Spacing.xs) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
.font(.caption)
|
||||
Text(String(localized: "editor.framerate30fps"))
|
||||
.font(.caption)
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
HStack(spacing: DesignTokens.Spacing.xs) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
.font(.caption)
|
||||
Text(String(localized: "editor.codecH264"))
|
||||
.font(.caption)
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
HStack(spacing: DesignTokens.Spacing.xs) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
.font(.caption)
|
||||
@@ -496,7 +502,7 @@ struct EditorView: View {
|
||||
}
|
||||
}
|
||||
.foregroundColor(.textSecondary)
|
||||
.padding(.leading, 4)
|
||||
.padding(.leading, DesignTokens.Spacing.xs)
|
||||
}
|
||||
}
|
||||
.padding(DesignTokens.Spacing.lg)
|
||||
@@ -507,7 +513,7 @@ struct EditorView: View {
|
||||
// MARK: - 诊断建议
|
||||
@ViewBuilder
|
||||
private func diagnosisSection(diagnosis: VideoDiagnosis) -> some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.yellow)
|
||||
@@ -517,12 +523,12 @@ struct EditorView: View {
|
||||
|
||||
ForEach(diagnosis.suggestions.indices, id: \.self) { index in
|
||||
let suggestion = diagnosis.suggestions[index]
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
HStack(alignment: .top, spacing: DesignTokens.Spacing.md) {
|
||||
Image(systemName: suggestion.icon)
|
||||
.foregroundStyle(suggestion.iconColor)
|
||||
.frame(width: 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
VStack(alignment: .leading, spacing: DesignTokens.Spacing.xs) {
|
||||
Text(suggestion.title)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
@@ -555,22 +561,15 @@ struct EditorView: View {
|
||||
// MARK: - 生成按钮
|
||||
@ViewBuilder
|
||||
private var generateButton: some View {
|
||||
Button {
|
||||
SoftPrimaryButton(
|
||||
String(localized: "editor.generateButton"),
|
||||
icon: "wand.and.stars",
|
||||
gradient: Color.gradientPrimary
|
||||
) {
|
||||
startProcessing()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "wand.and.stars")
|
||||
Text(String(localized: "editor.generateButton"))
|
||||
}
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.gradientPrimary)
|
||||
.foregroundColor(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
}
|
||||
.buttonStyle(ScaleButtonStyle())
|
||||
.padding(.top, 8)
|
||||
.padding(.top, DesignTokens.Spacing.sm)
|
||||
.padding(.bottom, DesignTokens.Spacing.sm)
|
||||
}
|
||||
|
||||
// MARK: - 方法
|
||||
@@ -758,6 +757,7 @@ struct EditorView: View {
|
||||
}
|
||||
|
||||
private func startProcessing() {
|
||||
generateTapCount += 1
|
||||
Analytics.shared.log(.editorGenerateClick, parameters: [
|
||||
"trimStart": trimStart,
|
||||
"trimEnd": trimEnd,
|
||||
@@ -795,15 +795,15 @@ struct AspectRatioButton: View {
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
VStack(spacing: 4) {
|
||||
VStack(spacing: DesignTokens.Spacing.xs) {
|
||||
// 比例图标
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
RoundedRectangle(cornerRadius: DesignTokens.Spacing.xs)
|
||||
.stroke(isSelected ? Color.accentPurple : Color.textSecondary, lineWidth: 2)
|
||||
.frame(width: iconWidth, height: iconHeight)
|
||||
.background(
|
||||
isSelected ? Color.accentPurple.opacity(0.1) : Color.clear
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.clipShape(RoundedRectangle(cornerRadius: DesignTokens.Spacing.xs))
|
||||
|
||||
Text(template.displayName)
|
||||
.font(.caption2)
|
||||
@@ -814,6 +814,7 @@ struct AspectRatioButton: View {
|
||||
.padding(.vertical, DesignTokens.Spacing.sm)
|
||||
.background(isSelected ? Color.accentPurple.opacity(0.1) : Color.clear)
|
||||
.clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.sm))
|
||||
.animation(DesignTokens.Animation.quick, value: isSelected)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityElement(children: .ignore)
|
||||
@@ -858,17 +859,18 @@ struct CropOverlay: View {
|
||||
.mask(
|
||||
Rectangle()
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
RoundedRectangle(cornerRadius: DesignTokens.Radius.sm)
|
||||
.frame(width: cropSize.width, height: cropSize.height)
|
||||
.blendMode(.destinationOut)
|
||||
)
|
||||
)
|
||||
|
||||
// 裁剪框边框
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
RoundedRectangle(cornerRadius: DesignTokens.Radius.sm)
|
||||
.stroke(Color.white, lineWidth: 2)
|
||||
.frame(width: cropSize.width, height: cropSize.height)
|
||||
}
|
||||
.animation(DesignTokens.Animation.standard, value: aspectRatio)
|
||||
}
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user