feat: navi header

This commit is contained in:
Junhui Chen 2025-08-19 14:49:52 +08:00
parent 10e9324049
commit f361d73bf7
7 changed files with 688 additions and 0 deletions

View File

@ -0,0 +1,14 @@
<?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

Binary file not shown.

View File

@ -0,0 +1,121 @@
//
// 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))
}

View File

@ -0,0 +1,163 @@
//
// 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))
}

View File

@ -6,6 +6,7 @@ enum FontFamily: String, CaseIterable {
case sankeiCute = "SankeiCutePopanime" //
case quicksand = "Quicksand x" //
case quicksandBold = "Quicksand-Bold"
case quicksandRegular = "Quicksand-Regular"
// case
// : case anotherFont = "AnotherFontName"

View File

@ -0,0 +1,389 @@
//
// 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()
}