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:
empty
2026-02-07 21:12:37 +08:00
parent 1556dfd167
commit a75aeed767

View File

@@ -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)
}