diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..17dc782 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "lldb.library": "/Library/Developer/CommandLineTools/Library/PrivateFrameworks/LLDB.framework/Versions/A/LLDB", + "lldb.launch.expressions": "native" +} \ No newline at end of file diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate index a86f52b..6e6e724 100644 Binary files a/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate and b/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/wake/Extensions/ColorExtensions.swift b/wake/Extensions/ColorExtensions.swift new file mode 100644 index 0000000..d70934b --- /dev/null +++ b/wake/Extensions/ColorExtensions.swift @@ -0,0 +1,34 @@ +// +// 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 + ) + } +} diff --git a/wake/Theme.swift b/wake/Theme.swift new file mode 100644 index 0000000..7a3f307 --- /dev/null +++ b/wake/Theme.swift @@ -0,0 +1,177 @@ +// +// 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) + } + } + } + } + } +} diff --git a/wake/View/Subscribe/Components/SubscriptionStatusBar.swift b/wake/View/Subscribe/Components/SubscriptionStatusBar.swift new file mode 100644 index 0000000..1a92267 --- /dev/null +++ b/wake/View/Subscribe/Components/SubscriptionStatusBar.swift @@ -0,0 +1,140 @@ +// +// 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)) +}