feat: 主题色

This commit is contained in:
Junhui Chen 2025-08-19 15:24:53 +08:00
parent f361d73bf7
commit 7d40fe3203
5 changed files with 355 additions and 0 deletions

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"lldb.library": "/Library/Developer/CommandLineTools/Library/PrivateFrameworks/LLDB.framework/Versions/A/LLDB",
"lldb.launch.expressions": "native"
}

View File

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

177
wake/Theme.swift Normal file
View File

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

View File

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