Compare commits
No commits in common. "831c7afd71ae53a43b8e0948456280d6aaa010a7" and "18147895a2a3ab62606a5dcc7e4675f3daafd869" have entirely different histories.
831c7afd71
...
18147895a2
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"lldb.library": "/Library/Developer/CommandLineTools/Library/PrivateFrameworks/LLDB.framework/Versions/A/LLDB",
|
|
||||||
"lldb.launch.expressions": "native"
|
|
||||||
}
|
|
||||||
Binary file not shown.
@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>SchemeUserState</key>
|
|
||||||
<dict>
|
|
||||||
<key>wake.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
BIN
wake/.DS_Store
vendored
BIN
wake/.DS_Store
vendored
Binary file not shown.
@ -1,121 +0,0 @@
|
|||||||
//
|
|
||||||
// ReturnButton.swift
|
|
||||||
// wake
|
|
||||||
//
|
|
||||||
// Created by Junhui on 2025/8/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
/// 返回按钮组件,用于左上角导航返回
|
|
||||||
struct ReturnButton: View {
|
|
||||||
let action: () -> Void
|
|
||||||
var iconName: String = "chevron.left"
|
|
||||||
var iconSize: TypographyStyle = .title
|
|
||||||
var iconColor: Color = .primary
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: action) {
|
|
||||||
Image(systemName: iconName)
|
|
||||||
.font(Typography.font(for: iconSize))
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(iconColor)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 带文字的返回按钮组件
|
|
||||||
struct ReturnButtonWithText: View {
|
|
||||||
let action: () -> Void
|
|
||||||
let text: String
|
|
||||||
var iconName: String = "chevron.left"
|
|
||||||
var spacing: CGFloat = 4
|
|
||||||
var textStyle: TypographyStyle = .body
|
|
||||||
var iconColor: Color = .primary
|
|
||||||
var textColor: Color = .primary
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: action) {
|
|
||||||
HStack(spacing: spacing) {
|
|
||||||
Image(systemName: iconName)
|
|
||||||
.font(Typography.font(for: textStyle, family: .quicksandRegular))
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(iconColor)
|
|
||||||
|
|
||||||
Text(text)
|
|
||||||
.font(Typography.font(for: textStyle, family: .quicksandRegular))
|
|
||||||
.foregroundColor(textColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 圆形背景的返回按钮组件
|
|
||||||
struct CircularReturnButton: View {
|
|
||||||
let action: () -> Void
|
|
||||||
var iconName: String = "chevron.left"
|
|
||||||
var size: CGFloat = 40
|
|
||||||
var backgroundColor: Color = Color(.systemBackground)
|
|
||||||
var iconColor: Color = .primary
|
|
||||||
var shadowRadius: CGFloat = 4
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: action) {
|
|
||||||
Image(systemName: iconName)
|
|
||||||
.font(Typography.font(for: .body, family: .quicksandRegular))
|
|
||||||
.foregroundColor(iconColor)
|
|
||||||
.frame(width: size, height: size)
|
|
||||||
.background(backgroundColor)
|
|
||||||
.clipShape(Circle())
|
|
||||||
.shadow(color: Color.black.opacity(0.1), radius: shadowRadius, x: 0, y: 2)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview("基础返回按钮") {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
HStack {
|
|
||||||
ReturnButton {
|
|
||||||
print("返回")
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview("带文字返回按钮") {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
HStack {
|
|
||||||
ReturnButtonWithText(action: {
|
|
||||||
print("返回")
|
|
||||||
}, text: "Back")
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview("圆形返回按钮") {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
HStack {
|
|
||||||
CircularReturnButton {
|
|
||||||
print("返回")
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
}
|
|
||||||
@ -1,163 +0,0 @@
|
|||||||
//
|
|
||||||
// NaviHeader.swift
|
|
||||||
// wake
|
|
||||||
//
|
|
||||||
// Created by Junhui on 2025/8/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
/// 导航头组件,包含返回按钮和标题
|
|
||||||
struct NaviHeader: View {
|
|
||||||
let title: String
|
|
||||||
let onBackTap: () -> Void
|
|
||||||
var showBackButton: Bool = true
|
|
||||||
var titleStyle: TypographyStyle = .title
|
|
||||||
var backgroundColor: Color = Color.clear
|
|
||||||
var rightContent: AnyView? = nil
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
// 标题居中显示
|
|
||||||
Text(title)
|
|
||||||
.font(Typography.font(for: titleStyle, family: .quicksandBold))
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
// 左右按钮层
|
|
||||||
HStack {
|
|
||||||
// 左侧返回按钮
|
|
||||||
if showBackButton {
|
|
||||||
ReturnButton(action: onBackTap)
|
|
||||||
} else {
|
|
||||||
Color.clear
|
|
||||||
.frame(width: 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 右侧内容
|
|
||||||
if let rightContent = rightContent {
|
|
||||||
rightContent
|
|
||||||
} else {
|
|
||||||
Color.clear
|
|
||||||
.frame(width: 30)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.top, 10)
|
|
||||||
.padding(.bottom, 20)
|
|
||||||
.background(backgroundColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 带右侧按钮的导航头组件
|
|
||||||
struct NaviHeaderWithAction: View {
|
|
||||||
let title: String
|
|
||||||
let onBackTap: () -> Void
|
|
||||||
let rightButtonTitle: String
|
|
||||||
let onRightButtonTap: () -> Void
|
|
||||||
var showBackButton: Bool = true
|
|
||||||
var titleStyle: TypographyStyle = .title
|
|
||||||
var rightButtonStyle: TypographyStyle = .body
|
|
||||||
var backgroundColor: Color = Color.clear
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
// 标题居中显示
|
|
||||||
Text(title)
|
|
||||||
.font(Typography.font(for: titleStyle, family: .quicksandBold))
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
// 左右按钮层
|
|
||||||
HStack {
|
|
||||||
// 左侧返回按钮
|
|
||||||
if showBackButton {
|
|
||||||
ReturnButton(action: onBackTap)
|
|
||||||
} else {
|
|
||||||
Color.clear
|
|
||||||
.frame(width: 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 右侧按钮
|
|
||||||
Button(action: onRightButtonTap) {
|
|
||||||
Text(rightButtonTitle)
|
|
||||||
.font(Typography.font(for: rightButtonStyle, family: .quicksandBold))
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.top, 10)
|
|
||||||
.padding(.bottom, 20)
|
|
||||||
.background(backgroundColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 简洁版导航头组件
|
|
||||||
struct SimpleNaviHeader: View {
|
|
||||||
let title: String
|
|
||||||
let onBackTap: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
// 标题居中显示
|
|
||||||
Text(title)
|
|
||||||
.font(Typography.font(for: .title, family: .quicksandBold))
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
|
|
||||||
// 返回按钮左对齐
|
|
||||||
HStack {
|
|
||||||
ReturnButton(action: onBackTap)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.vertical, 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview("基础导航头") {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
NaviHeader(title: "Settings") {
|
|
||||||
print("返回")
|
|
||||||
}
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview("带右侧按钮导航头") {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
NaviHeaderWithAction(
|
|
||||||
title: "Profile",
|
|
||||||
onBackTap: { print("返回") },
|
|
||||||
rightButtonTitle: "Save",
|
|
||||||
onRightButtonTap: { print("保存") }
|
|
||||||
)
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview("简洁导航头") {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
SimpleNaviHeader(title: "About") {
|
|
||||||
print("返回")
|
|
||||||
}
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
//
|
|
||||||
// ColorExtensions.swift
|
|
||||||
// wake
|
|
||||||
//
|
|
||||||
// Created by fairclip on 2025/8/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: - Color Extension for Hex Colors
|
|
||||||
extension Color {
|
|
||||||
/// 通过十六进制字符串创建颜色
|
|
||||||
/// - Parameter hex: 十六进制颜色字符串 (例如: "FF5733", "FFF8DE")
|
|
||||||
init(hex: String) {
|
|
||||||
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
|
||||||
var int: UInt64 = 0
|
|
||||||
Scanner(string: hex).scanHexInt64(&int)
|
|
||||||
let a, r, g, b: UInt64
|
|
||||||
switch hex.count {
|
|
||||||
case 6: // RGB (24-bit)
|
|
||||||
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
|
||||||
default:
|
|
||||||
(a, r, g, b) = (1, 1, 1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.init(
|
|
||||||
.sRGB,
|
|
||||||
red: Double(r) / 255,
|
|
||||||
green: Double(g) / 255,
|
|
||||||
blue: Double(b) / 255,
|
|
||||||
opacity: Double(a) / 255
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
177
wake/Theme.swift
177
wake/Theme.swift
@ -1,177 +0,0 @@
|
|||||||
//
|
|
||||||
// Theme.swift
|
|
||||||
// wake
|
|
||||||
//
|
|
||||||
// Created by fairclip on 2025/8/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: - 主题色管理
|
|
||||||
struct Theme {
|
|
||||||
|
|
||||||
// MARK: - 主要颜色
|
|
||||||
struct Colors {
|
|
||||||
|
|
||||||
// MARK: - 品牌色
|
|
||||||
static let primary = Color(hex: "FFB645") // 主橙色
|
|
||||||
static let primaryLight = Color(hex: "FFF8DE") // 浅橙色
|
|
||||||
static let primaryDark = Color(hex: "E6A03D") // 深橙色
|
|
||||||
|
|
||||||
// MARK: - 辅助色
|
|
||||||
static let secondary = Color(hex: "6C7B7F") // 灰蓝色
|
|
||||||
static let accent = Color(hex: "FF6B6B") // 强调红色
|
|
||||||
|
|
||||||
// MARK: - 中性色
|
|
||||||
static let background = Color(hex: "F8F9FA") // 背景色
|
|
||||||
static let surface = Color.white // 表面色
|
|
||||||
static let surfaceSecondary = Color(hex: "F5F5F5") // 次级表面色
|
|
||||||
|
|
||||||
// MARK: - 文本色
|
|
||||||
static let textPrimary = Color.black // 主文本色
|
|
||||||
static let textSecondary = Color(hex: "6B7280") // 次级文本色
|
|
||||||
static let textTertiary = Color(hex: "9CA3AF") // 三级文本色
|
|
||||||
static let textInverse = Color.white // 反色文本
|
|
||||||
|
|
||||||
// MARK: - 状态色
|
|
||||||
static let success = Color(hex: "10B981") // 成功色
|
|
||||||
static let warning = Color(hex: "F59E0B") // 警告色
|
|
||||||
static let error = Color(hex: "EF4444") // 错误色
|
|
||||||
static let info = Color(hex: "3B82F6") // 信息色
|
|
||||||
|
|
||||||
// MARK: - 边框色
|
|
||||||
static let border = Color(hex: "E5E7EB") // 边框色
|
|
||||||
static let borderLight = Color(hex: "F3F4F6") // 浅边框色
|
|
||||||
static let borderDark = Color(hex: "D1D5DB") // 深边框色
|
|
||||||
|
|
||||||
// MARK: - 订阅相关色
|
|
||||||
static let freeBackground = primaryLight // Free版背景
|
|
||||||
static let pioneerBackground = primary // Pioneer版背景
|
|
||||||
static let subscribeButton = primary // 订阅按钮色
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 渐变色
|
|
||||||
struct Gradients {
|
|
||||||
static let primaryGradient = LinearGradient(
|
|
||||||
colors: [Colors.primary, Colors.primaryDark],
|
|
||||||
startPoint: .topLeading,
|
|
||||||
endPoint: .bottomTrailing
|
|
||||||
)
|
|
||||||
|
|
||||||
static let backgroundGradient = LinearGradient(
|
|
||||||
colors: [Colors.background, Colors.surface],
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
|
|
||||||
static let accentGradient = LinearGradient(
|
|
||||||
colors: [Colors.accent, Color(hex: "FF8E8E")],
|
|
||||||
startPoint: .leading,
|
|
||||||
endPoint: .trailing
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 阴影
|
|
||||||
struct Shadows {
|
|
||||||
static let small = Color.black.opacity(0.1)
|
|
||||||
static let medium = Color.black.opacity(0.15)
|
|
||||||
static let large = Color.black.opacity(0.2)
|
|
||||||
|
|
||||||
// 阴影配置
|
|
||||||
static let cardShadow = (color: small, radius: CGFloat(4), x: CGFloat(0), y: CGFloat(2))
|
|
||||||
static let buttonShadow = (color: medium, radius: CGFloat(6), x: CGFloat(0), y: CGFloat(3))
|
|
||||||
static let modalShadow = (color: large, radius: CGFloat(12), x: CGFloat(0), y: CGFloat(8))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 圆角
|
|
||||||
struct CornerRadius {
|
|
||||||
static let small: CGFloat = 8
|
|
||||||
static let medium: CGFloat = 12
|
|
||||||
static let large: CGFloat = 16
|
|
||||||
static let extraLarge: CGFloat = 20
|
|
||||||
static let round: CGFloat = 50
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 间距
|
|
||||||
struct Spacing {
|
|
||||||
static let xs: CGFloat = 4
|
|
||||||
static let sm: CGFloat = 8
|
|
||||||
static let md: CGFloat = 12
|
|
||||||
static let lg: CGFloat = 16
|
|
||||||
static let xl: CGFloat = 20
|
|
||||||
static let xxl: CGFloat = 24
|
|
||||||
static let xxxl: CGFloat = 32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 便捷扩展
|
|
||||||
extension Color {
|
|
||||||
/// 主题色快捷访问
|
|
||||||
static var themePrimary: Color { Theme.Colors.primary }
|
|
||||||
static var themePrimaryLight: Color { Theme.Colors.primaryLight }
|
|
||||||
static var themeSecondary: Color { Theme.Colors.secondary }
|
|
||||||
static var themeAccent: Color { Theme.Colors.accent }
|
|
||||||
static var themeBackground: Color { Theme.Colors.background }
|
|
||||||
static var themeSurface: Color { Theme.Colors.surface }
|
|
||||||
static var themeTextPrimary: Color { Theme.Colors.textPrimary }
|
|
||||||
static var themeTextSecondary: Color { Theme.Colors.textSecondary }
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 预览
|
|
||||||
#Preview("Theme Colors") {
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: Theme.Spacing.lg) {
|
|
||||||
// 品牌色
|
|
||||||
ColorPreviewSection(title: "品牌色", colors: [
|
|
||||||
("Primary", Theme.Colors.primary),
|
|
||||||
("Primary Light", Theme.Colors.primaryLight),
|
|
||||||
("Primary Dark", Theme.Colors.primaryDark)
|
|
||||||
])
|
|
||||||
|
|
||||||
// 辅助色
|
|
||||||
ColorPreviewSection(title: "辅助色", colors: [
|
|
||||||
("Secondary", Theme.Colors.secondary),
|
|
||||||
("Accent", Theme.Colors.accent)
|
|
||||||
])
|
|
||||||
|
|
||||||
// 状态色
|
|
||||||
ColorPreviewSection(title: "状态色", colors: [
|
|
||||||
("Success", Theme.Colors.success),
|
|
||||||
("Warning", Theme.Colors.warning),
|
|
||||||
("Error", Theme.Colors.error),
|
|
||||||
("Info", Theme.Colors.info)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
.background(Theme.Colors.background)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 预览辅助组件
|
|
||||||
struct ColorPreviewSection: View {
|
|
||||||
let title: String
|
|
||||||
let colors: [(String, Color)]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
|
||||||
Text(title)
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(Theme.Colors.textPrimary)
|
|
||||||
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: Theme.Spacing.sm) {
|
|
||||||
ForEach(colors, id: \.0) { name, color in
|
|
||||||
VStack(spacing: Theme.Spacing.xs) {
|
|
||||||
Rectangle()
|
|
||||||
.fill(color)
|
|
||||||
.frame(height: 60)
|
|
||||||
.cornerRadius(Theme.CornerRadius.small)
|
|
||||||
|
|
||||||
Text(name)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(Theme.Colors.textSecondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,7 +6,6 @@ enum FontFamily: String, CaseIterable {
|
|||||||
case sankeiCute = "SankeiCutePopanime" // 可爱风格字体
|
case sankeiCute = "SankeiCutePopanime" // 可爱风格字体
|
||||||
case quicksand = "Quicksand x" // 主题字体
|
case quicksand = "Quicksand x" // 主题字体
|
||||||
case quicksandBold = "Quicksand-Bold"
|
case quicksandBold = "Quicksand-Bold"
|
||||||
case quicksandRegular = "Quicksand-Regular"
|
|
||||||
// 后续添加新字体库时在这里添加新 case
|
// 后续添加新字体库时在这里添加新 case
|
||||||
// 例如: case anotherFont = "AnotherFontName"
|
// 例如: case anotherFont = "AnotherFontName"
|
||||||
|
|
||||||
|
|||||||
@ -1,140 +0,0 @@
|
|||||||
//
|
|
||||||
// SubscriptionStatusBar.swift
|
|
||||||
// wake
|
|
||||||
//
|
|
||||||
// Created by fairclip on 2025/8/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: - 订阅状态枚举
|
|
||||||
enum SubscriptionStatus {
|
|
||||||
case free
|
|
||||||
case pioneer(expiryDate: Date)
|
|
||||||
|
|
||||||
var title: String {
|
|
||||||
switch self {
|
|
||||||
case .free:
|
|
||||||
return "Free"
|
|
||||||
case .pioneer:
|
|
||||||
return "Pioneer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasExpiry: Bool {
|
|
||||||
switch self {
|
|
||||||
case .free:
|
|
||||||
return false
|
|
||||||
case .pioneer:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var backgroundColor: Color {
|
|
||||||
switch self {
|
|
||||||
case .free:
|
|
||||||
return Theme.Colors.freeBackground // 浅橙色背景
|
|
||||||
case .pioneer:
|
|
||||||
return Theme.Colors.pioneerBackground // 橙色背景
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var textColor: Color {
|
|
||||||
switch self {
|
|
||||||
case .free:
|
|
||||||
return Theme.Colors.textPrimary
|
|
||||||
case .pioneer:
|
|
||||||
return Theme.Colors.textPrimary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 订阅状态栏组件
|
|
||||||
struct SubscriptionStatusBar: View {
|
|
||||||
let status: SubscriptionStatus
|
|
||||||
let onSubscribeTap: (() -> Void)?
|
|
||||||
|
|
||||||
init(status: SubscriptionStatus, onSubscribeTap: (() -> Void)? = nil) {
|
|
||||||
self.status = status
|
|
||||||
self.onSubscribeTap = onSubscribeTap
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
// 订阅类型标题
|
|
||||||
Text(status.title)
|
|
||||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
|
||||||
.foregroundColor(status.textColor)
|
|
||||||
|
|
||||||
// 过期时间或订阅按钮
|
|
||||||
if case .pioneer(let expiryDate) = status {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("Expires on :")
|
|
||||||
.font(.system(size: 14, weight: .medium))
|
|
||||||
.foregroundColor(status.textColor.opacity(0.8))
|
|
||||||
|
|
||||||
Text(formatDate(expiryDate))
|
|
||||||
.font(.system(size: 16, weight: .semibold))
|
|
||||||
.foregroundColor(status.textColor)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button(action: {
|
|
||||||
onSubscribeTap?()
|
|
||||||
}) {
|
|
||||||
Text("Subscribe")
|
|
||||||
.font(.system(size: 14, weight: .semibold))
|
|
||||||
.foregroundColor(Theme.Colors.textPrimary)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.background(Theme.Colors.subscribeButton)
|
|
||||||
.cornerRadius(Theme.CornerRadius.large)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 播放按钮图标
|
|
||||||
Circle()
|
|
||||||
.fill(Color.black)
|
|
||||||
.frame(width: 60, height: 60)
|
|
||||||
.overlay(
|
|
||||||
Image(systemName: "play.fill")
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.font(.title2)
|
|
||||||
.offset(x: 2) // 微调播放图标位置
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(20)
|
|
||||||
.background(status.backgroundColor)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.shadow(color: Color.black.opacity(0.1), radius: 4, x: 0, y: 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 日期格式化
|
|
||||||
private func formatDate(_ date: Date) -> String {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateFormat = "MMMM d, yyyy"
|
|
||||||
return formatter.string(from: date)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 预览
|
|
||||||
#Preview("Free Status") {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
SubscriptionStatusBar(
|
|
||||||
status: .free,
|
|
||||||
onSubscribeTap: {
|
|
||||||
print("Subscribe tapped")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
SubscriptionStatusBar(
|
|
||||||
status: .pioneer(expiryDate: Calendar.current.date(byAdding: .month, value: 6, to: Date()) ?? Date())
|
|
||||||
)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
}
|
|
||||||
@ -1,389 +0,0 @@
|
|||||||
//
|
|
||||||
// SubscribeView.swift
|
|
||||||
// wake
|
|
||||||
//
|
|
||||||
// Created by fairclip on 2025/8/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: - 订阅计划枚举
|
|
||||||
enum SubscriptionPlan: String, CaseIterable {
|
|
||||||
case free = "Free"
|
|
||||||
case pioneer = "Pioneer"
|
|
||||||
|
|
||||||
var displayName: String {
|
|
||||||
return self.rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
var price: String {
|
|
||||||
switch self {
|
|
||||||
case .free:
|
|
||||||
return "Free"
|
|
||||||
case .pioneer:
|
|
||||||
return "1$/Mon"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isPopular: Bool {
|
|
||||||
return self == .pioneer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 功能特性结构体
|
|
||||||
struct SubscriptionFeature {
|
|
||||||
let name: String
|
|
||||||
let freeValue: String
|
|
||||||
let proValue: String
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SubscribeView: View {
|
|
||||||
@State private var selectedPlan: SubscriptionPlan = .free
|
|
||||||
@State private var isLoading = false
|
|
||||||
@Environment(\.presentationMode) var presentationMode
|
|
||||||
|
|
||||||
// 功能对比数据
|
|
||||||
private let features = [
|
|
||||||
SubscriptionFeature(name: "Mystery Box Purchase:", freeValue: "3 /week", proValue: "Free"),
|
|
||||||
SubscriptionFeature(name: "Material Upload:", freeValue: "50 images and\n5 videos/day", proValue: "Unlimited"),
|
|
||||||
SubscriptionFeature(name: "Free Credits:", freeValue: "200 /day", proValue: "500 /day")
|
|
||||||
]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
// 导航栏
|
|
||||||
navigationHeader
|
|
||||||
|
|
||||||
// 当前订阅状态卡片
|
|
||||||
currentSubscriptionCard
|
|
||||||
|
|
||||||
// 积分信息
|
|
||||||
creditsSection
|
|
||||||
|
|
||||||
// 订阅计划选择
|
|
||||||
subscriptionPlansSection
|
|
||||||
|
|
||||||
// 特别优惠提示
|
|
||||||
specialOfferBanner
|
|
||||||
|
|
||||||
// 功能对比表
|
|
||||||
featureComparisonTable
|
|
||||||
|
|
||||||
// 订阅按钮
|
|
||||||
subscribeButton
|
|
||||||
|
|
||||||
// 法律链接
|
|
||||||
legalLinks
|
|
||||||
|
|
||||||
Spacer(minLength: 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
.navigationBarHidden(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 导航栏
|
|
||||||
private var navigationHeader: some View {
|
|
||||||
NaviHeader(title: "Subscription") {
|
|
||||||
presentationMode.wrappedValue.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 当前订阅状态卡片
|
|
||||||
private var currentSubscriptionCard: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("Free")
|
|
||||||
.font(Typography.font(for: .headline, family: .quicksand))
|
|
||||||
.fontWeight(.bold)
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
// 订阅操作
|
|
||||||
}) {
|
|
||||||
Text("Subscribe")
|
|
||||||
.font(Typography.font(for: .subtitle, family: .quicksand))
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(.black)
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.background(Color.orange)
|
|
||||||
.cornerRadius(20)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 播放按钮图标
|
|
||||||
Circle()
|
|
||||||
.fill(Color.black)
|
|
||||||
.frame(width: 60, height: 60)
|
|
||||||
.overlay(
|
|
||||||
Image(systemName: "play.fill")
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.font(.title2)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(20)
|
|
||||||
.background(Color.orange.opacity(0.2))
|
|
||||||
.cornerRadius(16)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 积分信息
|
|
||||||
private var creditsSection: some View {
|
|
||||||
HStack {
|
|
||||||
Text("Credits: 3290")
|
|
||||||
.font(Typography.font(for: .body, family: .quicksand))
|
|
||||||
.fontWeight(.medium)
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
// 积分信息操作
|
|
||||||
}) {
|
|
||||||
Image(systemName: "info.circle")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Image(systemName: "chevron.right")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.vertical, 16)
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
.cornerRadius(12)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.top, 20)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 订阅计划选择
|
|
||||||
private var subscriptionPlansSection: some View {
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
// Free 计划
|
|
||||||
SubscriptionPlanCard(
|
|
||||||
plan: .free,
|
|
||||||
isSelected: selectedPlan == .free,
|
|
||||||
onTap: { selectedPlan = .free }
|
|
||||||
)
|
|
||||||
|
|
||||||
// Pioneer 计划
|
|
||||||
SubscriptionPlanCard(
|
|
||||||
plan: .pioneer,
|
|
||||||
isSelected: selectedPlan == .pioneer,
|
|
||||||
onTap: { selectedPlan = .pioneer }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.top, 20)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 特别优惠横幅
|
|
||||||
private var specialOfferBanner: some View {
|
|
||||||
Text("First 100 users get a special deal: just $1 for your first month!")
|
|
||||||
.font(Typography.font(for: .caption, family: .quicksand))
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.top, 12)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 功能对比表
|
|
||||||
private var featureComparisonTable: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
// 表头
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text("Free")
|
|
||||||
.font(Typography.font(for: .subtitle, family: .quicksand))
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
Text("Pro")
|
|
||||||
.font(Typography.font(for: .subtitle, family: .quicksand))
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 16)
|
|
||||||
.background(Color(.systemGray6))
|
|
||||||
|
|
||||||
// 功能行
|
|
||||||
ForEach(Array(features.enumerated()), id: \.offset) { index, feature in
|
|
||||||
FeatureRow(feature: feature, isLast: index == features.count - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
.cornerRadius(12)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.top, 20)
|
|
||||||
.shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 订阅按钮
|
|
||||||
private var subscribeButton: some View {
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
Button(action: {
|
|
||||||
handleSubscribe()
|
|
||||||
}) {
|
|
||||||
if isLoading {
|
|
||||||
HStack {
|
|
||||||
ProgressView()
|
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
|
||||||
.scaleEffect(0.8)
|
|
||||||
Text("Subscribe")
|
|
||||||
.font(Typography.font(for: .body, family: .quicksand))
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text("Subscribe")
|
|
||||||
.font(Typography.font(for: .body, family: .quicksand))
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(.vertical, 16)
|
|
||||||
.background(Color.blue)
|
|
||||||
.cornerRadius(25)
|
|
||||||
.disabled(isLoading)
|
|
||||||
|
|
||||||
Text("Get 5,000 Permanent Credits")
|
|
||||||
.font(Typography.font(for: .caption, family: .quicksand))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 法律链接
|
|
||||||
private var legalLinks: some View {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Button("Terms of Service") {
|
|
||||||
// 打开服务条款
|
|
||||||
}
|
|
||||||
|
|
||||||
Text("|")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Button("Privacy Policy") {
|
|
||||||
// 打开隐私政策
|
|
||||||
}
|
|
||||||
|
|
||||||
Text("|")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Button("Restore Purchase") {
|
|
||||||
// 恢复购买
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.font(Typography.font(for: .caption, family: .quicksand))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.padding(.top, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 订阅处理
|
|
||||||
private func handleSubscribe() {
|
|
||||||
isLoading = true
|
|
||||||
|
|
||||||
// 模拟订阅处理
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
|
||||||
isLoading = false
|
|
||||||
// 处理订阅逻辑
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 订阅计划卡片
|
|
||||||
struct SubscriptionPlanCard: View {
|
|
||||||
let plan: SubscriptionPlan
|
|
||||||
let isSelected: Bool
|
|
||||||
let onTap: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: onTap) {
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
if plan.isPopular {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text("Popular")
|
|
||||||
.font(Typography.font(for: .caption, family: .quicksand))
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
.background(Color.black)
|
|
||||||
.cornerRadius(12)
|
|
||||||
}
|
|
||||||
.padding(.top, -8)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(plan.displayName)
|
|
||||||
.font(Typography.font(for: .title, family: .quicksand))
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(plan == .pioneer ? .white : .gray)
|
|
||||||
|
|
||||||
Text(plan.price)
|
|
||||||
.font(Typography.font(for: .body, family: .quicksand))
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(plan == .pioneer ? .white : .gray)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, minHeight: 120)
|
|
||||||
.padding(16)
|
|
||||||
.background(plan == .pioneer ? Color.orange : Color.white)
|
|
||||||
.cornerRadius(16)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 16)
|
|
||||||
.stroke(isSelected ? Color.blue : (plan == .free ? Color.blue : Color.clear), lineWidth: 2)
|
|
||||||
)
|
|
||||||
.shadow(color: Color.black.opacity(0.1), radius: 4, x: 0, y: 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 功能对比行
|
|
||||||
struct FeatureRow: View {
|
|
||||||
let feature: SubscriptionFeature
|
|
||||||
let isLast: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
Text(feature.name)
|
|
||||||
.font(Typography.font(for: .subtitle, family: .quicksand))
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
Text(feature.freeValue)
|
|
||||||
.font(Typography.font(for: .caption, family: .quicksand))
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
|
|
||||||
Text(feature.proValue)
|
|
||||||
.font(Typography.font(for: .caption, family: .quicksand))
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
|
|
||||||
if !isLast {
|
|
||||||
Divider()
|
|
||||||
.padding(.leading, 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
SubscribeView()
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user