wake-ios/wake/View/Credits/CreditsDetailView.swift

291 lines
10 KiB
Swift

//
// CreditsDetailView.swift
// wake
//
// Created by fairclip on 2025/8/19.
//
import SwiftUI
// MARK: -
enum CreditTransactionType: String, CaseIterable {
case photoUnderstanding = "Photo Understanding"
case videoUnderstanding = "Video Understanding"
case mysteryBoxPurchase = "Mystery Box Purchase"
case dailyBonus = "Daily Bonus"
case subscriptionBonus = "Subscription Bonus"
var creditChange: Int {
switch self {
case .photoUnderstanding:
return -1
case .videoUnderstanding:
return -32
case .mysteryBoxPurchase:
return -100
case .dailyBonus:
return 200
case .subscriptionBonus:
return 500
}
}
var icon: String {
switch self {
case .photoUnderstanding:
return "photo"
case .videoUnderstanding:
return "video"
case .mysteryBoxPurchase:
return "gift"
case .dailyBonus:
return "calendar"
case .subscriptionBonus:
return "star.fill"
}
}
}
// MARK: -
struct CreditTransaction {
let id = UUID()
let type: CreditTransactionType
let date: Date
let creditChange: Int
init(type: CreditTransactionType, date: Date, creditChange: Int? = nil) {
self.type = type
self.date = date
self.creditChange = creditChange ?? type.creditChange
}
}
// MARK: -
struct CreditsDetailView: View {
@Environment(\.presentationMode) var presentationMode
@State private var showRules = false
//
private let totalCredits = 3290
private let expiringToday = 200
private let transactions: [CreditTransaction] = [
CreditTransaction(type: .photoUnderstanding, date: Calendar.current.date(byAdding: .hour, value: -2, to: Date()) ?? Date()),
CreditTransaction(type: .videoUnderstanding, date: Calendar.current.date(byAdding: .hour, value: -4, to: Date()) ?? Date()),
CreditTransaction(type: .mysteryBoxPurchase, date: Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date()),
CreditTransaction(type: .dailyBonus, date: Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date()),
CreditTransaction(type: .subscriptionBonus, date: Calendar.current.date(byAdding: .day, value: -2, to: Date()) ?? Date())
]
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 0) {
//
navigationHeader
//
mainCreditsCard
//
creditsHistorySection
Spacer(minLength: 100)
}
}
.background(Theme.Colors.background)
.navigationBarHidden(true)
}
}
// MARK: -
private var navigationHeader: some View {
NaviHeader(title: "Credits") {
presentationMode.wrappedValue.dismiss()
}
}
// MARK: -
private var mainCreditsCard: some View {
VStack(spacing: 0) {
//
HStack {
//
Circle()
.fill(Color.black)
.frame(width: 80, height: 80)
.overlay(
Image(systemName: "triangle.fill")
.foregroundColor(.white)
.font(.system(size: 24, weight: .bold))
)
Spacer()
//
VStack(alignment: .trailing, spacing: 8) {
HStack(spacing: 8) {
Circle()
.fill(Color.black)
.frame(width: 24, height: 24)
.overlay(
Image(systemName: "triangle.fill")
.foregroundColor(.white)
.font(.system(size: 8))
)
Text("\(totalCredits)")
.font(Typography.font(for: .headline, family: .quicksandBold, size: 36))
.foregroundColor(.black)
}
Text("Expiring Today : \(expiringToday)")
.font(Typography.font(for: .body, family: .quicksand))
.foregroundColor(.black.opacity(0.8))
}
}
.padding(Theme.Spacing.xl)
// 线
DashedLine()
.stroke(Color.black.opacity(0.3), style: StrokeStyle(lineWidth: 1, dash: [5, 5]))
.frame(height: 1)
.padding(.horizontal, Theme.Spacing.xl)
//
creditsRulesSection
}
.background(
LinearGradient(
colors: [
Color(hex: "FFB645"),
Color(hex: "FFA726")
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.cornerRadius(Theme.CornerRadius.large)
.padding(.horizontal, Theme.Spacing.xl)
.padding(.top, Theme.Spacing.xl)
}
// MARK: -
private var creditsRulesSection: some View {
VStack(spacing: 0) {
//
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
showRules.toggle()
}
}) {
HStack {
Text("Credits Rules")
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(.black)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.black)
.font(.system(size: 14, weight: .medium))
.rotationEffect(.degrees(showRules ? 90 : 0))
.animation(.easeInOut(duration: 0.3), value: showRules)
}
.padding(.horizontal, Theme.Spacing.xl)
.padding(.vertical, Theme.Spacing.lg)
}
//
if showRules {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Credits can be used for material indexing (1 credit per photo or per second of video) and for buying blind boxes (100 credits each).")
.font(Typography.font(for: .subtitle, family: .quicksand))
.foregroundColor(.black.opacity(0.8))
.multilineTextAlignment(.leading)
}
.padding(.horizontal, Theme.Spacing.xl)
.padding(.bottom, Theme.Spacing.lg)
}
}
}
// MARK: -
private var creditsHistorySection: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.lg) {
Text("Points History")
.font(Typography.font(for: .title, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
.padding(.horizontal, Theme.Spacing.xxl)
LazyVStack(spacing: 0) {
ForEach(Array(transactions.enumerated()), id: \.element.id) { index, transaction in
CreditTransactionRow(
transaction: transaction,
isLast: index == transactions.count - 1
)
}
}
.background(Color(.systemBackground))
.cornerRadius(Theme.CornerRadius.medium)
.padding(.horizontal, Theme.Spacing.xl)
}
.padding(.top, Theme.Spacing.xl)
}
}
// MARK: -
struct CreditTransactionRow: View {
let transaction: CreditTransaction
let isLast: Bool
var body: some View {
VStack(spacing: 0) {
HStack(spacing: Theme.Spacing.lg) {
VStack(alignment: .leading, spacing: 4) {
Text(transaction.type.rawValue)
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
Text(formatDate(transaction.date))
.font(Typography.font(for: .caption, family: .quicksand))
.foregroundColor(Theme.Colors.textSecondary)
}
Spacer()
Text("\(transaction.creditChange > 0 ? "+" : "")\(transaction.creditChange)")
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(transaction.creditChange > 0 ? Theme.Colors.success : Theme.Colors.textPrimary)
}
.padding(.horizontal, Theme.Spacing.lg)
.padding(.vertical, Theme.Spacing.lg)
if !isLast {
Divider()
.background(Theme.Colors.borderLight)
}
}
}
private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "MM-dd-yyyy"
return formatter.string(from: date)
}
}
// MARK: - 线
struct DashedLine: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
return path
}
}
// MARK: -
#Preview {
CreditsDetailView()
}