Merge branch 'settingView'

This commit is contained in:
Junhui Chen 2025-08-20 15:42:51 +08:00
commit f15f70cc8c
46 changed files with 3521 additions and 640 deletions

BIN
.DS_Store vendored

Binary file not shown.

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

@ -8,7 +8,7 @@
/* Begin PBXBuildFile section */
AB8773632E4E04400071CB53 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = AB8773622E4E040E0071CB53 /* .gitignore */; };
ABB4E2182E4B761400660198 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = ABB4E2172E4B761400660198 /* Alamofire */; };
ABE8998E2E533A7100CD7BA6 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = ABE8998D2E533A7100CD7BA6 /* Alamofire */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@ -27,11 +27,25 @@
/* Begin PBXFileReference section */
AB8773622E4E040E0071CB53 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
ABB4E2082E4B75D900660198 /* wake.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = wake.app; sourceTree = BUILT_PRODUCTS_DIR; };
ABE899102E5328F100CD7BA6 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
AB4FA8642E4F7074005D9955 /* Exceptions for "wake" folder in "wake" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = ABB4E2072E4B75D900660198 /* wake */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
ABB4E20A2E4B75D900660198 /* wake */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
AB4FA8642E4F7074005D9955 /* Exceptions for "wake" folder in "wake" target */,
);
path = wake;
sourceTree = "<group>";
};
@ -42,7 +56,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
ABB4E2182E4B761400660198 /* Alamofire in Frameworks */,
ABE8998E2E533A7100CD7BA6 /* Alamofire in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -54,6 +68,7 @@
children = (
AB8773622E4E040E0071CB53 /* .gitignore */,
ABB4E20A2E4B75D900660198 /* wake */,
ABE8990F2E5328F100CD7BA6 /* Frameworks */,
ABB4E2092E4B75D900660198 /* Products */,
);
sourceTree = "<group>";
@ -66,6 +81,14 @@
name = Products;
sourceTree = "<group>";
};
ABE8990F2E5328F100CD7BA6 /* Frameworks */ = {
isa = PBXGroup;
children = (
ABE899102E5328F100CD7BA6 /* CoreData.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -87,7 +110,7 @@
);
name = wake;
packageProductDependencies = (
ABB4E2172E4B761400660198 /* Alamofire */,
ABE8998D2E533A7100CD7BA6 /* Alamofire */,
);
productName = wake;
productReference = ABB4E2082E4B75D900660198 /* wake.app */;
@ -118,7 +141,7 @@
mainGroup = ABB4E1FF2E4B75D900660198;
minimizedProjectReferenceProxies = 1;
packageReferences = (
ABB4E2162E4B761400660198 /* XCRemoteSwiftPackageReference "Alamofire" */,
ABE8998C2E533A7100CD7BA6 /* XCRemoteSwiftPackageReference "Alamofire" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = ABB4E2092E4B75D900660198 /* Products */;
@ -278,11 +301,15 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = wake/wake.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GB3VPJ54BD;
DEVELOPMENT_TEAM = 392N3QB7XR;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = wake/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "";
INFOPLIST_KEY_NSUserTrackingUsageDescription = "我们需要访问您的Apple ID信息来为您提供登录服务";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -292,7 +319,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.memowake.wake;
PRODUCT_BUNDLE_IDENTIFIER = com.memowake.fairclip2025;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@ -305,11 +332,15 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = wake/wake.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GB3VPJ54BD;
DEVELOPMENT_TEAM = 392N3QB7XR;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = wake/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "";
INFOPLIST_KEY_NSUserTrackingUsageDescription = "我们需要访问您的Apple ID信息来为您提供登录服务";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -319,7 +350,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.memowake.wake;
PRODUCT_BUNDLE_IDENTIFIER = com.memowake.fairclip2025;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@ -351,7 +382,7 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
ABB4E2162E4B761400660198 /* XCRemoteSwiftPackageReference "Alamofire" */ = {
ABE8998C2E533A7100CD7BA6 /* XCRemoteSwiftPackageReference "Alamofire" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Alamofire/Alamofire.git";
requirement = {
@ -362,9 +393,9 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
ABB4E2172E4B761400660198 /* Alamofire */ = {
ABE8998D2E533A7100CD7BA6 /* Alamofire */ = {
isa = XCSwiftPackageProductDependency;
package = ABB4E2162E4B761400660198 /* XCRemoteSwiftPackageReference "Alamofire" */;
package = ABE8998C2E533A7100CD7BA6 /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire;
};
/* End XCSwiftPackageProductDependency section */

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ABB4E2072E4B75D900660198"
BuildableName = "wake.app"
BlueprintName = "wake"
ReferencedContainer = "container:wake.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ABB4E2072E4B75D900660198"
BuildableName = "wake.app"
BlueprintName = "wake"
ReferencedContainer = "container:wake.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<StoreKitConfigurationFileReference
identifier = "../../wake/MemoWake.storekit">
</StoreKitConfigurationFileReference>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ABB4E2072E4B75D900660198"
BuildableName = "wake.app"
BlueprintName = "wake"
ReferencedContainer = "container:wake.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "55F37A93-4556-4005-B9BD-8F1A1D6A8474"
type = "1"
version = "2.0">
</Bucket>

View File

@ -0,0 +1,22 @@
<?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>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>ABB4E2072E4B75D900660198</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>

BIN
wake/.DS_Store vendored

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 184 KiB

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

@ -1,228 +1,233 @@
import SwiftUI
import SwiftData
//
// MARK: -
extension AnyTransition {
///
static var slideFromLeading: AnyTransition {
.asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .leading).combined(with: .opacity)
insertion: .move(edge: .trailing).combined(with: .opacity), //
removal: .move(edge: .leading).combined(with: .opacity) //
)
}
}
// 1.
enum Route: Hashable {
case settings
}
// MARK: -
struct ContentView: View {
@State private var showModal = false
@State private var showSettings = false
@State private var navigationPath = NavigationPath()
@State private var contentOffset: CGFloat = 0
// MARK: -
@State private var showModal = false //
@State private var showSettings = false //
@State private var contentOffset: CGFloat = 0 //
@State private var showLogin = false
var body: some View {
NavigationStack(path: $navigationPath) {
// NavigationStack
let _ = Self._printChanges()
let _ = print("Navigation path changed: \(navigationPath)")
//
@Environment(\.modelContext) private var modelContext
// -
@Query private var login: [Login]
// MARK: -
var body: some View {
NavigationView {
ZStack {
//
VStack {
VStack(spacing: 20) {
// This spacer ensures content stays below the status bar
//
Spacer().frame(height: UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
//
//
HStack {
Spacer()
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showModal = true
}
}) {
//
Button(action: showUserProfile) {
Image(systemName: "gearshape")
.font(.title2)
.padding()
}
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemBackground))
.offset(x: showModal ? UIScreen.main.bounds.width * 0.35 : 0)
.animation(.spring(response: 0.5, dampingFraction: 0.8), value: showModal)
.edgesIgnoringSafeArea(.all)
}
//
if showModal {
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showModal = false
}
}
.transition(.opacity)
}
// Modal with animation - will be pushed off-screen by SettingsView
SlideInModal(isPresented: $showModal, onDismiss: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showModal = false
}
}) {
// Modal content
// Modal content with offset for SettingsView
VStack(spacing: 20) {
//
HStack(alignment: .center, spacing: 16) {
//
Image(systemName: "person.circle.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
.foregroundColor(.blue)
.clipShape(Circle())
// ID
VStack(alignment: .leading, spacing: 4) {
Text("用户名")
.font(.headline)
.foregroundColor(.primary)
Text("ID: 12345678")
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
Text("Wake")
.font(.largeTitle)
.fontWeight(.bold)
.onTapGesture {
if login.isEmpty {
print("⚠️ 没有登录记录,正在创建新记录...")
let newLogin = Login(
email: "jyq@example.com",
name: "New User"
)
modelContext.insert(newLogin)
try? modelContext.save()
print("✅ 已创建新登录记录")
} else if let firstLogin = login.first {
// 2.
print("🔍 找到现有记录,正在更新...")
firstLogin.email = "updated@example.com"
firstLogin.name = "Updated Name"
try? modelContext.save()
print("✅ 记录已更新")
}
}
//
NavigationLink(destination: LoginView()) {
Text("登录")
.font(.headline)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
//
NavigationLink(destination: SubscribeView()) {
Text("Subscribe")
.font(.subheadline)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.orange)
.foregroundColor(.white)
.cornerRadius(8)
}
.padding(.trailing)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.top, 16)
VStack(alignment: .leading, spacing: 8) {
Text("会员等级")
.font(.headline)
.foregroundColor(.primary)
Text("会员时间")
.font(.subheadline)
.foregroundColor(.secondary)
Text("会员中心")
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(Color(red: 0.92, green: 0.92, blue: 0.92))
.cornerRadius(10)
.padding(.horizontal, 16)
VStack(spacing: 12) {
// memories
Button(action: {
print("memories")
}) {
HStack(spacing: 16) {
Image(systemName: "crown.fill")
.foregroundColor(.orange)
.frame(width: 24, height: 24)
Text("My Memories")
.font(.headline)
.foregroundColor(.primary)
Spacer()
}
.padding()
.cornerRadius(10)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
// Box
Button(action: {
print("Box")
}) {
HStack(spacing: 16) {
Image(systemName: "clock.fill")
.foregroundColor(.blue)
.frame(width: 24, height: 24)
Text("My Bind Box")
.font(.headline)
.foregroundColor(.primary)
Spacer()
//
List {
Section(header: Text("我的收藏")) {
ForEach(1...5, id: \.self) { item in
HStack {
Image(systemName: "photo")
.foregroundColor(.blue)
.frame(width: 40, height: 40)
.background(Color.blue.opacity(0.1))
.cornerRadius(8)
VStack(alignment: .leading, spacing: 4) {
Text("项目 \(item)")
.font(.headline)
Text("这是第\(item)个项目的描述")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
.padding(.vertical, 4)
}
}
Section(header: Text("最近活动")) {
ForEach(6...10, id: \.self) { item in
HStack {
Image(systemName: "clock")
.foregroundColor(.orange)
.frame(width: 40, height: 40)
.background(Color.orange.opacity(0.1))
.cornerRadius(8)
VStack(alignment: .leading, spacing: 4) {
Text("活动 \(item)")
.font(.headline)
Text("\(item)分钟前更新")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
Text("查看")
.font(.caption)
.padding(6)
.background(Color.blue.opacity(0.1))
.foregroundColor(.blue)
.cornerRadius(4)
}
.padding(.vertical, 4)
}
}
.padding()
.cornerRadius(10)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
// setting
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showSettings = true
}
}) {
HStack(spacing: 16) {
Image(systemName: "person.circle.fill")
.foregroundColor(.purple)
.frame(width: 24, height: 24)
Text("Setting")
.font(.headline)
.foregroundColor(.primary)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(10)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
.listStyle(GroupedListStyle())
.padding(.top, 0)
}
.padding(.horizontal, 16)
//
Spacer()
}
.padding(.vertical, 8)
}
// Apply offset to the entire modal when SettingsView is shown
.offset(x: showSettings ? UIScreen.main.bounds.width : 0)
.animation(.spring(response: 0.5, dampingFraction: 0.8), value: showSettings)
ZStack {
// Semi-transparent overlay for settings
if showSettings {
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showSettings = false
}
}
.transition(.opacity)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemBackground))
.offset(x: showModal ? UIScreen.main.bounds.width * 0.8 : 0)
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showModal)
.edgesIgnoringSafeArea(.all)
}
// Full screen settings view with slide animation
if showSettings {
SettingsView(isPresented: $showSettings)
.transition(.move(edge: .leading))
.zIndex(1) // Ensure it's above other content
.onAppear {
// Reset the navigation path when settings appear
navigationPath.removeLast(navigationPath.count)
}
//
SlideInModal(
isPresented: $showModal,
onDismiss: hideUserProfile
) {
UserProfileModal(
showModal: $showModal,
showSettings: $showSettings
)
}
.offset(x: showSettings ? UIScreen.main.bounds.width : 0)
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showSettings)
//
ZStack {
if showSettings {
Color.black.opacity(0.3)
.edgesIgnoringSafeArea(.all)
.onTapGesture(perform: hideSettings)
.transition(.opacity)
}
if showSettings {
SettingsView(isPresented: $showSettings)
.transition(.move(edge: .leading))
.zIndex(1)
}
}
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showSettings)
}
.animation(.spring(response: 0.5, dampingFraction: 0.8), value: showSettings)
.navigationBarHidden(true)
}
}
///
private func showUserProfile() {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
// print(": \(login.count)")
// for (index, item) in login.enumerated() {
// print(" \(index + 1): =\(item.email), =\(item.name)")
// }
print("当前登录记录:")
for (index, item) in login.enumerated() {
print("记录 \(index + 1): 邮箱=\(item.email), 姓名=\(item.name)")
}
}
}
}
// showModal.toggle()
}
}
///
private func hideUserProfile() {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
showModal = false
}
}
///
private func hideSettings() {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
showSettings = false
}
}
}
// MARK: -
#Preview {
ContentView()
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788.4" systemVersion="24G84" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="Entity" representedClassName="Entity" syncable="YES" codeGenerationType="class">
<attribute name="accessToken" optional="YES" attributeType="String"/>
<attribute name="fulname" optional="YES" attributeType="String"/>
<attribute name="refreshToken" optional="YES" attributeType="String"/>
<attribute name="username" optional="YES" attributeType="String"/>
</entity>
</model>

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

View File

@ -0,0 +1,22 @@
import SwiftUI
// MARK: -
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return Path(path.cgPath)
}
}
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
}

34
wake/Info.plist Normal file
View File

@ -0,0 +1,34 @@
<?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>ASAuthorizationAppleIDProvider</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>appleid</string>
<string>appleauth</string>
</array>
<key>NSAppleIDUsageDescription</key>
<string>Sign in with Apple is used to authenticate your account</string>
<key>UIAppFonts</key>
<array>
<string>Quicksand x.ttf</string>
<string>SankeiCutePopanime.ttf</string>
<string>Quicksand-Regular.ttf</string>
<string>Quicksand-Bold.ttf</string>
<string>Quicksand-SemiBold.ttf</string>
<string>Quicksand-Medium.ttf</string>
<string>Quicksand-Light.ttf</string>
</array>
</dict>
</plist>

215
wake/MemoWake.storekit Normal file
View File

@ -0,0 +1,215 @@
{
"appPolicies" : {
"eula" : "",
"policies" : [
{
"locale" : "en_US",
"policyText" : "",
"policyURL" : ""
}
]
},
"identifier" : "C75471B9",
"nonRenewingSubscriptions" : [
],
"products" : [
],
"settings" : {
"_applicationInternalID" : "6748205761",
"_developerTeamID" : "392N3QB7XR",
"_failTransactionsEnabled" : false,
"_lastSynchronizedDate" : 777364219.49411595,
"_locale" : "en_US",
"_storefront" : "USA",
"_storeKitErrors" : [
{
"current" : null,
"enabled" : false,
"name" : "Load Products"
},
{
"current" : null,
"enabled" : false,
"name" : "Purchase"
},
{
"current" : null,
"enabled" : false,
"name" : "Verification"
},
{
"current" : null,
"enabled" : false,
"name" : "App Store Sync"
},
{
"current" : null,
"enabled" : false,
"name" : "Subscription Status"
},
{
"current" : null,
"enabled" : false,
"name" : "App Transaction"
},
{
"current" : null,
"enabled" : false,
"name" : "Manage Subscriptions Sheet"
},
{
"current" : null,
"enabled" : false,
"name" : "Refund Request Sheet"
},
{
"current" : null,
"enabled" : false,
"name" : "Offer Code Redeem Sheet"
}
]
},
"subscriptionGroups" : [
{
"id" : "21759571",
"localizations" : [
],
"name" : "Membership",
"subscriptions" : [
{
"adHocOffers" : [
],
"codeOffers" : [
],
"displayPrice" : "0.99",
"familyShareable" : false,
"groupNumber" : 1,
"internalID" : "6751260055",
"introductoryOffer" : null,
"localizations" : [
{
"description" : "The Pioneer Plan unlocks many restrictions.",
"displayName" : "Pioneer Plan",
"locale" : "en_US"
},
{
"description" : "先锋计划用户不限制盲盒购买数量不限制回忆上传数量每天免费获取500积分",
"displayName" : "先锋计划",
"locale" : "zh_Hans"
}
],
"productID" : "MEMBERSHIP_PIONEER_MONTHLY",
"recurringSubscriptionPeriod" : "P1M",
"referenceName" : "Pioneer计划",
"subscriptionGroupID" : "21759571",
"type" : "RecurringSubscription",
"winbackOffers" : [
]
}
]
},
{
"id" : "21740727",
"localizations" : [
],
"name" : "Pro会员",
"subscriptions" : [
{
"adHocOffers" : [
],
"codeOffers" : [
],
"displayPrice" : "12.99",
"familyShareable" : false,
"groupNumber" : 1,
"internalID" : "6749133482",
"introductoryOffer" : null,
"localizations" : [
{
"description" : "Pro会员每月有更高的存储空间与积分数量",
"displayName" : "季度Pro会员",
"locale" : "zh_Hans"
}
],
"productID" : "MEMBERSHIP_PRO_QUARTERLY",
"recurringSubscriptionPeriod" : "P3M",
"referenceName" : "季度Pro会员",
"subscriptionGroupID" : "21740727",
"type" : "RecurringSubscription",
"winbackOffers" : [
]
},
{
"adHocOffers" : [
],
"codeOffers" : [
],
"displayPrice" : "59.99",
"familyShareable" : false,
"groupNumber" : 2,
"internalID" : "6749229999",
"introductoryOffer" : null,
"localizations" : [
{
"description" : "Pro会员每月有更高的存储空间与积分数量",
"displayName" : "年度Pro会员",
"locale" : "zh_Hans"
}
],
"productID" : "MEMBERSHIP_PRO_YEARLY",
"recurringSubscriptionPeriod" : "P1Y",
"referenceName" : "年度Pro会员",
"subscriptionGroupID" : "21740727",
"type" : "RecurringSubscription",
"winbackOffers" : [
]
},
{
"adHocOffers" : [
],
"codeOffers" : [
],
"displayPrice" : "3.99",
"familyShareable" : false,
"groupNumber" : 3,
"internalID" : "6749230171",
"introductoryOffer" : null,
"localizations" : [
{
"description" : "Pro会员每月有更高的存储空间与积分数量",
"displayName" : "月度Pro会员",
"locale" : "zh_Hans"
}
],
"productID" : "MEMBERSHIP_PRO_MONTH",
"recurringSubscriptionPeriod" : "P1M",
"referenceName" : "月度Pro会员",
"subscriptionGroupID" : "21740727",
"type" : "RecurringSubscription",
"winbackOffers" : [
]
}
]
}
],
"version" : {
"major" : 4,
"minor" : 0
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,22 @@
import Foundation
import SwiftData
@Model
final class Login {
@Attribute(.unique) //
//
// fulname
var name: String
//
var email: String
//
var description: String {
return "Login(name: \(name), email: \(email))"
}
//
init(email:String="", name: String = "") {
self.email = email
self.name = name
}
}

191
wake/Theme.swift Normal file
View File

@ -0,0 +1,191 @@
//
// 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: "FAFAFA") //
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: "D9D9D9") //
static let borderLight = Color(hex: "F3F4F6") //
static let borderBlack = Color.black //
static let borderDark = borderBlack //
// MARK: -
static let freeBackground = primaryLight // Free
static let pioneerBackground = primary // Pioneer
static let subscribeButton = primary //
// MARK: -
static let cardBackground = Color.white //
}
// MARK: -
struct Gradients {
static let primaryGradient = LinearGradient(
colors: [Colors.primary, Colors.primaryDark],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
static let backgroundGradient = LinearGradient(
gradient: Gradient(colors: [
Color(hex: "FBC063"),
Color(hex: "FEE9BE"),
Color(hex: "FAB851")
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
static let accentGradient = LinearGradient(
colors: [Colors.accent, Color(hex: "FF8E8E")],
startPoint: .leading,
endPoint: .trailing
)
// static let creditsInfoTooltip = LinearGradient(
// colors: [Colors(hex: "FFD38F"), Colors(hex: "FFF8DE"), Colors(hex: "FECE83")],
// startPoint: .topLeading,
// endPoint: .bottomTrailing
// )
}
// 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)
}
}
}
}
}
}

100
wake/Typography.swift Normal file
View File

@ -0,0 +1,100 @@
import SwiftUI
// MARK: -
///
enum FontFamily: String, CaseIterable {
case sankeiCute = "SankeiCutePopanime" //
case quicksandRegular = "Quicksand-Regular" //
case quicksandBold = "Quicksand-Bold"
// case
// : case anotherFont = "AnotherFontName"
///
var name: String {
return self.rawValue
}
}
// `.quicksand`
extension FontFamily {
@available(*, deprecated, message: "Use .quicksandRegular instead.")
static var quicksand: FontFamily { .quicksandRegular }
}
// MARK: -
/// 使
enum TypographyStyle {
case headline //
case title //
case body //
case subtitle //
case caption //
case footnote //
case small //
}
// MARK: -
private struct TypographyConfig {
let size: CGFloat
let weight: UIFont.Weight
let textStyle: UIFont.TextStyle
}
// MARK: - Typography
struct Typography {
// MARK: -
///
private static let defaultFontFamily: FontFamily = .quicksandRegular
///
private static let styleConfig: [TypographyStyle: TypographyConfig] = [
.headline: TypographyConfig(size: 24, weight: .bold, textStyle: .headline),
.title: TypographyConfig(size: 20, weight: .semibold, textStyle: .title2),
.body: TypographyConfig(size: 16, weight: .regular, textStyle: .body),
.subtitle: TypographyConfig(size: 14, weight: .medium, textStyle: .subheadline),
.caption: TypographyConfig(size: 12, weight: .light, textStyle: .caption1),
.footnote: TypographyConfig(size: 11, weight: .regular, textStyle: .footnote),
.small: TypographyConfig(size: 10, weight: .regular, textStyle: .headline)
]
// MARK: -
///
/// - Parameters:
/// - style:
/// - family: nil 使
/// - Returns: Font
static func font(for style: TypographyStyle, family: FontFamily? = nil, size: CGFloat? = nil) -> Font {
let fontFamily = family ?? defaultFontFamily
guard let config = styleConfig[style] else {
return .body
}
//
if let customFont = UIFont(name: fontFamily.name, size: size ?? config.size) {
let metrics = UIFontMetrics(forTextStyle: config.textStyle)
let scaledFont = metrics.scaledFont(for: customFont)
return Font(scaledFont)
}
// 退
let systemFont = UIFont.systemFont(ofSize: size ?? config.size, weight: config.weight)
let metrics = UIFontMetrics(forTextStyle: config.textStyle)
let scaledFont = metrics.scaledFont(for: systemFont)
return Font(scaledFont)
}
///
/// - Returns:
static func availableFonts() -> [FontFamily] {
return FontFamily.allCases
}
///
/// - Parameter family:
/// - Returns:
static func isFontAvailable(_ family: FontFamily) -> Bool {
return UIFont(name: family.name, size: 16) != nil
}
}

125
wake/Utils/IAPManager.swift Normal file
View File

@ -0,0 +1,125 @@
import Foundation
import StoreKit
@MainActor
final class IAPManager: ObservableObject {
@Published var isPurchasing: Bool = false
@Published var pioneerProduct: Product?
@Published var errorMessage: String?
@Published var isSubscribed: Bool = false
@Published var subscriptionExpiry: Date? = nil
// Product IDs from App Store Connect
private let productIDs: [String] = [
"MEMBERSHIP_PIONEER_MONTHLY"
]
init() {
Task { await observeTransactions() }
}
// Load products defined in App Store Connect
func loadProducts() async {
do {
let products = try await Product.products(for: productIDs)
// You can refine selection logic if you have multiple tiers
self.pioneerProduct = products.first
if products.isEmpty {
// No products found is a common setup issue (App Store Connect, StoreKit config, or bundle ID)
self.errorMessage = "No subscription products found. Please try again later."
}
} catch {
self.errorMessage = "Failed to load products: \(error.localizedDescription)"
}
}
¬
// Trigger App Store purchase sheet
func purchasePioneer() async {
guard !isPurchasing else { return }
guard let product = pioneerProduct else {
// Surface an actionable error so the UI can inform the user
self.errorMessage = "Subscription product unavailable. Please try again later."
return
}
isPurchasing = true
defer { isPurchasing = false }
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .unverified(_, let error):
self.errorMessage = "Purchase unverified: \(error.localizedDescription)"
case .verified(let transaction):
// Update entitlement for the purchased product
updateEntitlement(from: transaction)
await transaction.finish()
}
case .userCancelled:
break
case .pending:
break
@unknown default:
break
}
} catch {
self.errorMessage = "Purchase failed: \(error.localizedDescription)"
}
}
// Restore purchases (sync entitlements)
func restorePurchases() async {
do {
try await AppStore.sync()
} catch {
self.errorMessage = "Restore failed: \(error.localizedDescription)"
}
}
// Observe transaction updates for entitlement changes
private func observeTransactions() async {
for await result in Transaction.updates {
do {
let transaction: Transaction = try checkVerified(result)
// Update entitlement state for transaction.productID
updateEntitlement(from: transaction)
await transaction.finish()
} catch {
// Ignore unverified transactions
}
}
}
// Check current entitlements (useful on launch)
func refreshEntitlements() async {
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result,
productIDs.contains(transaction.productID) {
updateEntitlement(from: transaction)
}
}
}
// Helper: verify
private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified(_, let error):
throw error
case .verified(let safe):
return safe
}
}
private func updateEntitlement(from transaction: Transaction) {
guard productIDs.contains(transaction.productID) else { return }
// For auto-renewable subs, use expirationDate and revocationDate
if transaction.revocationDate == nil {
self.isSubscribed = true
self.subscriptionExpiry = transaction.expirationDate
} else {
self.isSubscribed = false
self.subscriptionExpiry = nil
}
}
}

View File

@ -16,8 +16,8 @@ struct SlideInModal<Content: View>: View {
ZStack(alignment: .leading) {
//
if isPresented {
Color.black
.opacity(0.5)
Color.clear
.contentShape(Rectangle())
.edgesIgnoringSafeArea(.all)
.transition(.opacity)
.zIndex(1)
@ -49,7 +49,7 @@ struct SlideInModal<Content: View>: View {
.animation(animation, value: isPresented)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.edgesIgnoringSafeArea(.all)
}
}

View File

@ -0,0 +1,122 @@
import SwiftUI
import PhotosUI
struct PhotoPicker: UIViewControllerRepresentable {
@Binding var selectedImages: [UIImage]
let selectionLimit: Int
let filter: PHPickerFilter
init(selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images) {
self._selectedImages = selectedImages
self.selectionLimit = selectionLimit
self.filter = filter
}
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
configuration.filter = filter
configuration.selectionLimit = selectionLimit
configuration.preferredAssetRepresentationMode = .current
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
let parent: PhotoPicker
init(_ parent: PhotoPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
parent.selectedImages.removeAll()
let group = DispatchGroup()
var loadedImages: [Int: UIImage] = [:]
for (index, result) in results.enumerated() {
group.enter()
if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
if let image = image as? UIImage {
loadedImages[index] = image
}
group.leave()
}
} else {
group.leave()
}
}
group.notify(queue: .main) {
// Sort the images by their original index to maintain selection order
let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value }
self.parent.selectedImages.append(contentsOf: sortedImages)
// Dismiss the picker
picker.dismiss(animated: true)
}
}
}
}
// MARK: - Avatar Uploader Component
struct AvatarUploader: View {
@Binding var selectedImage: UIImage?
let size: CGFloat
@State private var isImagePickerPresented = false
var body: some View {
Button(action: {
isImagePickerPresented = true
}) {
ZStack {
// Avatar Image or Placeholder
if let selectedImage = selectedImage {
Image(uiImage: selectedImage)
.resizable()
.scaledToFill()
.frame(width: size, height: size)
.clipShape(RoundedRectangle(cornerRadius: size * 0.1))
} else {
// Default avatar container
Color.gray.opacity(0.1)
.frame(width: size, height: size)
.overlay(
SVGImage(svgName: "Avatar")
.frame(width: size * 0.8, height: size * 0.8)
)
.clipShape(RoundedRectangle(cornerRadius: size * 0.1))
.overlay(
RoundedRectangle(cornerRadius: size * 0.1)
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
)
}
}
.frame(width: size, height: size)
.contentShape(Rectangle()) // Make the entire area tappable
}
.buttonStyle(PlainButtonStyle()) // Remove button highlight effect
.sheet(isPresented: $isImagePickerPresented) {
PhotoPicker(
selectedImages: Binding(
get: { [selectedImage].compactMap { $0 } },
set: { images in
selectedImage = images.first
}
),
selectionLimit: 1
)
}
}
}

View File

@ -0,0 +1,135 @@
import SwiftUI
struct UserProfileModal: View {
@Binding var showModal: Bool
@Binding var showSettings: Bool
var body: some View {
// Modal content with transparent background
VStack(spacing: 20) {
Spacer()
.frame(height: UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
//
HStack(alignment: .center, spacing: 16) {
//
Image(systemName: "person.circle.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
.foregroundColor(.blue)
.clipShape(Circle())
// ID
VStack(alignment: .leading, spacing: 4) {
Text("用户名")
.font(.headline)
.foregroundColor(.primary)
Text("ID: 12345678")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.top, 16)
VStack(alignment: .leading, spacing: 8) {
Text("会员等级")
.font(.headline)
.foregroundColor(.primary)
Text("会员时间")
.font(.subheadline)
.foregroundColor(.secondary)
Text("会员中心")
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(Color.clear)
.padding(.horizontal, 16)
VStack(spacing: 12) {
// memories
Button(action: {
print("memories")
}) {
HStack(spacing: 16) {
Image(systemName: "crown.fill")
.foregroundColor(.orange)
.frame(width: 24, height: 24)
Text("My Memories")
.font(.headline)
.foregroundColor(.primary)
Spacer()
}
.padding()
.cornerRadius(10)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
// Box
Button(action: {
print("Box")
}) {
HStack(spacing: 16) {
Image(systemName: "clock.fill")
.foregroundColor(.blue)
.frame(width: 24, height: 24)
Text("My Bind Box")
.font(.headline)
.foregroundColor(.primary)
Spacer()
}
.padding()
.cornerRadius(10)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
// setting
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showSettings = true
}
}) {
HStack(spacing: 16) {
Image(systemName: "person.circle.fill")
.foregroundColor(.purple)
.frame(width: 24, height: 24)
Text("Setting")
.font(.headline)
.foregroundColor(.primary)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
.padding()
.background(Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
}
.padding(.horizontal, 16)
Spacer()
}
.frame(width: UIScreen.main.bounds.width * 0.8)
.background(Color(red: 0.87, green: 0.87, blue: 0.87))
.cornerRadius(20, corners: [.topRight, .bottomRight])
.edgesIgnoringSafeArea(.all)
}
}
#Preview {
UserProfileModal(showModal: .constant(true), showSettings: .constant(false))
}

View File

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

View File

@ -0,0 +1,107 @@
//
// CreditsInfoCard.swift
// wake
//
// Created by fairclip on 2025/8/19.
//
import SwiftUI
// MARK: -
struct CreditsInfoCard: View {
let totalCredits: Int
let onInfoTap: (() -> Void)?
let onDetailTap: (() -> Void)?
@State private var showInfoPopover = false
init(
totalCredits: Int,
onInfoTap: (() -> Void)? = nil,
onDetailTap: (() -> Void)? = nil
) {
self.totalCredits = totalCredits
self.onInfoTap = onInfoTap
self.onDetailTap = onDetailTap
}
var body: some View {
Button(action: {
onDetailTap?()
}) {
mainCreditsSection
}
.buttonStyle(PlainButtonStyle())
.background(Theme.Colors.primaryLight)
.cornerRadius(Theme.CornerRadius.extraLarge)
.shadow(color: Theme.Shadows.small, radius: Theme.Shadows.cardShadow.radius, x: Theme.Shadows.cardShadow.x, y: Theme.Shadows.cardShadow.y)
}
// MARK: -
private var mainCreditsSection: some View {
HStack(spacing: Theme.Spacing.md) {
//
HStack(spacing: Theme.Spacing.sm) {
Text("Credits:")
.font(Typography.font(for: .subtitle, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
Text("\(totalCredits)")
.font(Typography.font(for: .subtitle, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
}
//
HStack(spacing: Theme.Spacing.sm) {
//
Button(action: {
showInfoPopover = true
onInfoTap?()
}) {
Image(systemName: "questionmark.circle")
.foregroundColor(Theme.Colors.textSecondary)
.font(.system(size: 16))
}
.popover(isPresented: $showInfoPopover, attachmentAnchor: .point(.bottom), arrowEdge: .top) {
Text("Credits can be used for material indexing (1 credit per photo or per second of video) and for buying blind boxes (100 crediteach)")
.font(Typography.font(for: .caption, family: .quicksandRegular))
.multilineTextAlignment(.center)
.presentationBackground(Theme.Gradients.backgroundGradient)
.frame(minWidth: 200, maxWidth: UIScreen.main.bounds.width * 0.6)
.presentationCompactAdaptation(.popover)
.padding(.horizontal, Theme.Spacing.md)
.padding(.vertical, Theme.Spacing.sm)
}
Spacer()
//
Image(systemName: "chevron.right")
.foregroundColor(Theme.Colors.textPrimary)
.font(.system(size: 14, weight: .medium))
}
}
.padding(Theme.Spacing.lg)
}
}
// MARK: -
#Preview("Credits Info Card") {
VStack(spacing: 20) {
CreditsInfoCard(
totalCredits: 3290,
onInfoTap: {
print("Info tapped")
},
onDetailTap: {
print("Detail tapped")
}
)
}
.padding()
.background(Color(.systemGroupedBackground))
}

View File

@ -1,270 +1,318 @@
import SwiftUI
import AuthenticationServices
import Alamofire
import CryptoKit
struct Post: Codable {
let id: Int
let title: String
let body: String
let userId: Int
}
struct Login: Encodable {
let account: String
let password: String
}
/// -
struct LoginView: View {
@State private var showModal = false
@State private var showSettings = false
@State private var contentOffset: CGFloat = 0
// /
@State private var username=""
//
@State private var password=""
// loading
@State private var isLoading=false
// MARK: - Properties
//
func handleLogin() {
withAnimation {
isLoading = true
}
//
passwordLogin(username: "jyq@memo.cn", password: "111111")
// isLoading = false
// passwordLogin
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation {
self.isLoading = false
}
}
}
@State private var isLoading = false
@State private var showError = false
@State private var errorMessage = ""
@State private var currentNonce: String?
@State private var isLoggedIn = false
// get
func get(){
AF.request("http://192.168.31.156:31646/api/v1/iam/access-token-refresh").response { response in
debugPrint(response)
}
}
// post
func post(){
let login = Login(account: username, password: password)
print(login)
AF.request("http://192.168.31.156:31646/api/v1/iam/login/password-login", method: .post,parameters: login).response{
response in debugPrint(response)
}
}
// MARK: - Body
// func createPost() {
// Task {
// do {
// print("12132412354365342")
// let newPost = try await NetworkManager.shared.request(
// endpoint: "/iam/login/password-login",
// method: .post,
// parameters: [
// "account": username,
// "password": password
// ]
// ) as Post
// // \()
// print("$newPost.id)\(username)")
// } catch {
// print("$error)")
//
// }
// }
// }
var body: some View {
ZStack {
// Main content with slide effect
VStack {
VStack(spacing: 20) {
// This spacer ensures content stays below the status bar
Spacer().frame(height: UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
//
HStack {
Spacer()
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showModal = true
}
}) {
Image(systemName: "gearshape")
.font(.title2)
.padding()
}
}
Text("邮箱登录").font(.title)
//
VStack {
//
CustomTextField(
placeholder: "请输入用户名",
type: .username,
value: $username
)
//
CustomTextField(
placeholder: "请输入密码",
type: .password,
value: $password
)
//
CustomButton(
text: "登录",
type: .primary,
size: .large,
fullWidth: true,
isLoading: isLoading
) {
handleLogin()
}
}
.padding()
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemBackground))
.offset(x: showModal ? UIScreen.main.bounds.width * 0.35 : 0)
.animation(.spring(response: 0.5, dampingFraction: 0.8), value: showModal)
.edgesIgnoringSafeArea(.all)
}
//
if showModal {
Color.black.opacity(0.4)
NavigationStack {
ZStack {
// Background
Color(red: 1.0, green: 0.67, blue: 0.15)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showModal = false
}
}
.transition(.opacity)
}
// Modal with animation
SlideInModal(isPresented: $showModal, onDismiss: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showModal = false
}
}) {
VStack(spacing: 20) {
//
HStack(alignment: .center, spacing: 16) {
//
Image(systemName: "person.circle.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
.foregroundColor(.blue)
.clipShape(Circle())
// ID
VStack(alignment: .leading, spacing: 4) {
Text("用户名")
.font(.headline)
.foregroundColor(.primary)
Text("ID: 12345678")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.top, 16)
VStack(alignment: .leading, spacing: 4) {
Text("Hi, I'm MeMo!")
.font(.largeTitle)
.fontWeight(.semibold)
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 24)
.padding(.top, 44)
Text("Welcome~")
.font(.largeTitle)
.fontWeight(.semibold)
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 24)
.padding(.bottom, 20)
VStack(alignment: .leading, spacing: 8) {
Text("会员等级")
.font(.headline)
.foregroundColor(.primary)
Text("会员时间")
.font(.subheadline)
.foregroundColor(.secondary)
Text("会员中心")
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(Color(red: 0.92, green: 0.92, blue: 0.92))
.cornerRadius(10)
.padding(.horizontal, 16)
VStack(spacing: 12) {
// memories
Button(action: {
print("memories")
}) {
HStack(spacing: 16) {
Image(systemName: "crown.fill")
.foregroundColor(.orange)
.frame(width: 24, height: 24)
Text("My Memories")
.font(.headline)
.foregroundColor(.primary)
Spacer()
}
.padding()
.cornerRadius(10)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
// Box
Button(action: {
print("Box")
}) {
HStack(spacing: 16) {
Image(systemName: "clock.fill")
.foregroundColor(.blue)
.frame(width: 24, height: 24)
Text("My Bind Box")
.font(.headline)
.foregroundColor(.primary)
Spacer()
}
.padding()
.cornerRadius(10)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
// setting
Button(action: {
print("Setting")
}) {
HStack(spacing: 16) {
Image(systemName: "person.circle.fill")
.foregroundColor(.purple)
.frame(width: 24, height: 24)
Text("Setting")
.font(.headline)
.foregroundColor(.primary)
Spacer()
}
.padding()
.cornerRadius(10)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
}
.padding(.horizontal, 16)
//
Spacer()
}
.padding(.vertical, 8)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
VStack(spacing: 16) {
Spacer()
signInButton()
termsAndPrivacyView()
}
.padding()
.alert(isPresented: $showError) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("OK"))
)
}
if isLoading {
loadingView()
}
}
.navigationBarHidden(true)
.fullScreenCover(isPresented: $isLoggedIn) {
NavigationStack {
UserInfo()
}
}
}
}
}
#Preview {
LoginView()
// MARK: - Views
private func signInButton() -> some View {
SignInWithAppleButton(
onRequest: { request in
let nonce = String.randomURLSafeString(length: 32)
self.currentNonce = nonce
request.requestedScopes = [.fullName, .email]
request.nonce = self.sha256(nonce)
},
onCompletion: handleAppleSignIn
)
.signInWithAppleButtonStyle(.white)
.frame(height: 50)
.cornerRadius(25)
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.black, lineWidth: 1)
)
}
private func termsAndPrivacyView() -> some View {
VStack(spacing: 4) {
HStack {
Text("By continuing, you agree to our")
.font(.caption)
.foregroundColor(.secondary)
Button("Terms of") {
openURL("https://yourwebsite.com/terms")
}
.font(.caption2)
.foregroundColor(.blue)
}
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
HStack(spacing: 8) {
Button("Service") {
openURL("https://yourwebsite.com/terms")
}
.font(.caption2)
.foregroundColor(.blue)
Text("and")
.foregroundColor(.secondary)
.font(.caption)
Button("Privacy Policy") {
openURL("https://yourwebsite.com/privacy")
}
.font(.caption2)
.foregroundColor(.blue)
}
.padding(.top, 4)
}
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity)
.padding(.horizontal, 24)
.padding(.bottom, 24)
}
private func loadingView() -> some View {
ZStack {
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.all)
ProgressView()
.scaleEffect(1.5)
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
}
// MARK: - Authentication
private func handleAppleSignIn(result: Result<ASAuthorization, Error>) {
print("🔵 [Apple Sign In] 开始处理登录结果...")
DispatchQueue.main.async {
self.isLoggedIn = true
}
switch result {
case .success(let authResults):
print("✅ [Apple Sign In] 登录授权成功")
processAppleIDCredential(authResults.credential)
case .failure(let error):
print("❌ [Apple Sign In] 登录失败: \(error.localizedDescription)")
handleSignInError(error)
}
}
private func processAppleIDCredential(_ credential: ASAuthorizationCredential) {
print("🔵 [Apple ID] 开始处理凭证...")
guard let appleIDCredential = credential as? ASAuthorizationAppleIDCredential else {
print("❌ [Apple ID] 凭证类型不匹配")
showError(message: "无法处理Apple ID凭证")
return
}
let userId = appleIDCredential.user
let email = appleIDCredential.email ?? ""
let fullName = [
appleIDCredential.fullName?.givenName,
appleIDCredential.fullName?.familyName
]
.compactMap { $0 }
.joined(separator: " ")
print(" [Apple ID] 用户数据 - ID: \(userId), 邮箱: \(email.isEmpty ? "未提供" : email), 姓名: \(fullName.isEmpty ? "未提供" : fullName)")
guard let identityTokenData = appleIDCredential.identityToken,
let identityToken = String(data: identityTokenData, encoding: .utf8) else {
print("❌ [Apple ID] 无法获取身份令牌")
showError(message: "无法获取身份令牌")
return
}
var authCode: String? = nil
if let authCodeData = appleIDCredential.authorizationCode {
authCode = String(data: authCodeData, encoding: .utf8)
print(" [Apple ID] 获取到授权码")
} else {
print(" [Apple ID] 未获取到授权码(可选)")
}
print("🔵 [Apple ID] 准备调用后端认证...")
authenticateWithBackend(
userId: userId,
email: email,
name: fullName,
identityToken: identityToken,
authCode: authCode
)
}
// MARK: - Network
private func authenticateWithBackend(
userId: String,
email: String,
name: String,
identityToken: String,
authCode: String?
) {
isLoading = true
print("🔵 [Backend] 开始后端认证...")
let url = "https://your-api-endpoint.com/api/auth/apple"
var parameters: [String: Any] = [
"appleUserId": userId,
"email": email,
"name": name,
"identityToken": identityToken
]
if let authCode = authCode {
parameters["authorizationCode"] = authCode
}
print("📤 [Backend] 请求参数: \(parameters)")
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default)
.validate()
.responseJSON { response in
self.isLoading = false
switch response.result {
case .success(let value):
print("✅ [Backend] 认证成功: \(value)")
self.handleSuccessfulAuthentication()
case .failure(let error):
print("❌ [Backend] 认证失败: \(error.localizedDescription)")
if let data = response.data, let json = String(data: data, encoding: .utf8) {
print("❌ [Backend] 错误详情: \(json)")
}
self.handleAuthenticationError(error)
}
}
}
// MARK: - Helpers
private func handleSuccessfulAuthentication() {
print("✅ [Auth] 登录成功,准备跳转到用户信息页面...")
DispatchQueue.main.async {
self.isLoggedIn = true
}
}
private func handleSignInError(_ error: Error) {
let errorMessage = (error as NSError).localizedDescription
print("❌ [Auth] 登录错误: \(errorMessage)")
showError(message: "登录失败: \(error.localizedDescription)")
}
private func handleAuthenticationError(_ error: AFError) {
let errorMessage = error.localizedDescription
print("❌ [Auth] 认证错误: \(errorMessage)")
DispatchQueue.main.async {
self.isLoggedIn = false
self.showError(message: "登录失败: \(errorMessage)")
}
}
private func showError(message: String) {
DispatchQueue.main.async {
self.errorMessage = message
self.showError = true
}
}
private func openURL(_ string: String) {
guard let url = URL(string: string) else { return }
UIApplication.shared.open(url)
}
private func sha256(_ input: String) -> String {
let inputData = Data(input.utf8)
let hashedData = SHA256.hash(data: inputData)
return hashedData.compactMap { String(format: "%02x", $0) }.joined()
}
}
// MARK: - Extensions
extension String {
static func randomURLSafeString(length: Int) -> String {
let characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"
var randomString = ""
for _ in 0..<length {
let randomIndex = Int.random(in: 0..<characters.count)
let character = characters[characters.index(characters.startIndex, offsetBy: randomIndex)]
randomString.append(character)
}
return randomString
}
}
// MARK: - Preview
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}

View File

@ -1,201 +1,133 @@
import SwiftUI
///
struct SettingsView: View {
// MARK: -
/// - dismiss
@Environment(\.dismiss) private var dismiss
@State private var isAppeared = false
/// - /
@Binding var isPresented: Bool
// Animation configuration
// MARK: -
///
private let animation = Animation.spring(
response: 0.8,
dampingFraction: 0.6,
blendDuration: 0.8
response: 0.6, //
dampingFraction: 0.9, //
blendDuration: 0.8 //
)
// MARK: -
var body: some View {
VStack(spacing: 0) {
// Custom navigation bar
HStack {
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
isPresented = false
}
}) {
HStack(spacing: 4) {
Image(systemName: "chevron.left")
.font(.system(size: 17, weight: .semibold))
Text("Back")
}
.foregroundColor(.blue)
.padding()
//
SimpleNaviHeader(title: "Setting") {
withAnimation(animation) {
isPresented = false
}
Spacer()
Text("Settings")
.font(.headline)
.padding()
Spacer()
// Invisible view to balance the layout
Color.clear
.frame(width: 44, height: 44)
}
.background(Color(.systemBackground))
// Settings content
List(0..<1) { _ in
// This empty section ensures proper spacing
Section {
EmptyView()
} header: {
EmptyView()
//
List {
//
settingRow(
icon: "person.crop.circle",
title: "Account & Security",
action: {}
)
//
settingRow(
icon: "lock.shield",
title: "Permission Management",
action: {}
)
//
settingRow(
icon: "questionmark.circle",
title: "Support & Service",
action: {}
)
//
settingRow(
icon: "info.circle",
title: "About Us",
action: {}
)
}
// Add an invisible section header to remove extra top padding
Section(header: EmptyView()) {
EmptyView()
}
// Account & Security
HStack {
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
Image(systemName: "person.crop.circle")
.font(.system(size: 24))
.foregroundColor(.gray)
Text("Account & Security")
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
}
.listRowBackground(Color(.systemBackground))
// Permission Management
HStack {
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
Image(systemName: "lock.shield")
.font(.system(size: 24))
.foregroundColor(.gray)
Text("Permission Management")
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
}
.listRowBackground(Color(.systemBackground))
// Support & Service
HStack {
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
Image(systemName: "questionmark.circle")
.font(.system(size: 24))
.foregroundColor(.gray)
Text("Support & Service")
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
}
.listRowBackground(Color(.systemBackground))
// About Us
HStack {
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
Image(systemName: "info.circle")
.font(.system(size: 24))
.foregroundColor(.gray)
Text("About Us")
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
}
.listRowBackground(Color(.systemBackground))
//
.listStyle(PlainListStyle())
// 线
.listRowSeparator(.hidden)
//
.listRowInsets(EdgeInsets())
.background(Color(.systemGroupedBackground))
}
.listStyle(GroupedListStyle())
.navigationTitle("Setting")
.navigationBarTitleDisplayMode(.inline)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemGray6))
.background(Color(.systemGroupedBackground))
// regular
.environment(\.horizontalSizeClass, .regular)
.environment(\.defaultMinListRowHeight, 50)
.listRowInsets(EdgeInsets())
.onAppear {
// Remove extra separators below the list
UITableView.appearance().tableFooterView = UIView()
// Remove separator inset
UITableView.appearance().separatorInset = .zero
// Remove extra space at the top of the table view
UITableView.appearance().contentInset = .zero
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
isAppeared = false
}
// Delay the dismiss to allow the animation to complete
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
isPresented = false
}
}) {
HStack(spacing: 4) {
Image(systemName: "chevron.left")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.blue)
Text("Back")
.font(.system(size: 17, weight: .regular))
.foregroundColor(.blue)
}
}
}
}
.animation(animation, value: isAppeared)
}
}
// MARK: -
/// TableView
private func configureTableView() {
// 线
UITableView.appearance().tableFooterView = UIView()
// 线
UITableView.appearance().tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNonzeroMagnitude))
// 线
UITableView.appearance().separatorInset = .zero
//
UITableView.appearance().contentInset = .zero
}
///
/// - Parameters:
/// - icon:
/// - title:
/// - action:
/// - Returns:
private func settingRow(icon: String, title: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack {
//
Image(systemName: icon)
.font(.system(size: 24))
.foregroundColor(.gray)
.frame(width: 40)
//
Text(title)
.foregroundColor(.primary)
Spacer()
//
Image(systemName: "chevron.right")
.font(.system(size: 14))
.foregroundColor(.gray)
}
.padding(.vertical, 6) //
.padding(.horizontal, 12)
.background(Color(.systemGroupedBackground))
}
.buttonStyle(PlainButtonStyle())
.listRowBackground(Color(.systemGroupedBackground))
.listRowSeparator(.hidden)
}
}
// MARK: - Preview
// MARK: -
#Preview {
NavigationView {
SettingsView(isPresented: .constant(true))
}
}
// MARK: - Subviews
struct AccountSecurityView: View {
var body: some View {
Text("Account & Security")
}
}
struct PermissionManagementView: View {
var body: some View {
Text("Permission Management")
}
}
struct SupportServiceView: View {
var body: some View {
Text("Support & Service")
}
}
struct AboutUsView: View {
var body: some View {
Text("About Us")
}
}

View File

@ -0,0 +1,153 @@
import SwiftUI
struct UserInfo: View {
@Environment(\.dismiss) private var dismiss
// Sample user data - replace with your actual data model
@State private var userName = "MeMo"
@State private var userEmail = "memo@example.com"
@State private var notificationsEnabled = true
@State private var darkModeEnabled = false
@State private var showLogoutAlert = false
@State private var avatarImage: UIImage? // Add this line
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 20) {
Text("Choose a photo as your avatar, and we'll generate a video mystery box for you.")
.font(Typography.font(for: .small))
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 10)
.background(
LinearGradient(
gradient: Gradient(colors: [
Color(red: 1.0, green: 0.97, blue: 0.87),
.white,
Color(red: 1.0, green: 0.97, blue: 0.84)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
.padding(.vertical, 10)
Spacer()
VStack(spacing: 20) {
// Title
Text("Add Your Avatar")
.font(Typography.font(for: .title))
.frame(maxWidth: .infinity, alignment: .center)
// Avatar
ZStack {
// Show either the SVG or the uploaded image
if let avatarImage = avatarImage {
Image(uiImage: avatarImage)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipShape(Circle())
} else {
SVGImage(svgName: "Avatar")
.frame(width: 200, height: 200)
}
// Make sure the AvatarUploader is on top and covers the entire area
AvatarUploader(selectedImage: $avatarImage, size: 200)
.contentShape(Rectangle()) // This makes the entire area tappable
}
.frame(width: 200, height: 200)
.padding(.vertical, 20)
// Buttons
Button(action: {
// Action for first button
}) {
Text("Upload from Gallery")
.frame(maxWidth: .infinity)
.padding()
.foregroundColor(.black)
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color(red: 1.0, green: 0.973, blue: 0.871))
)
}
Button(action: {
// Action for second button
}) {
Text("Take a Photo")
.frame(maxWidth: .infinity)
.padding()
.foregroundColor(.black)
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color(red: 1.0, green: 0.973, blue: 0.871))
)
}
}
.padding()
.background(Color(.white))
.cornerRadius(20)
Spacer()
Button(action: {
// Action for next button
}) {
Text("Next")
.frame(maxWidth: .infinity)
.padding()
.foregroundColor(.black)
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color(red: 1.0, green: 0.714, blue: 0.271))
)
}
}
.padding()
.navigationTitle("Complete Your Profile")
.navigationBarTitleDisplayMode(.inline)
.background(Color(red: 0.98, green: 0.98, blue: 0.98)) // #FAFAFA
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
dismiss()
}) {
Image(systemName: "chevron.left")
.foregroundColor(.blue)
}
}
}
}
}
// MARK: - Settings Row View
struct SettingsRow: View {
let icon: String
let title: String
let color: Color
var body: some View {
HStack {
Image(systemName: icon)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.padding(6)
.background(color.opacity(0.1))
.foregroundColor(color)
.cornerRadius(6)
Text(title)
.padding(.leading, 5)
}
.padding(.vertical, 4)
}
}
// MARK: - Preview
struct UserInfo_Previews: PreviewProvider {
static var previews: some View {
UserInfo()
}
}

View File

@ -0,0 +1,165 @@
//
// PlanCompare.swift
// wake
//
// Created by fairclip on 2025/8/20.
//
import SwiftUI
// MARK: -
struct PlanFeature {
let title: String
let subtitle: String?
let freeValue: String
let pioneerValue: String
let icon: String?
}
// MARK: -
struct PlanCompare: View {
// MARK: -
private let features: [PlanFeature] = [
PlanFeature(
title: "Mystery Box Purchase:",
subtitle: nil,
freeValue: "3 /week",
pioneerValue: "Unlimited",
icon: nil
),
PlanFeature(
title: "Material Upload:",
subtitle: nil,
freeValue: "50 images and\n5 videos/day",
pioneerValue: "Unlimited",
icon: nil
),
PlanFeature(
title: "Free Credits:",
subtitle: "Expires the next day",
freeValue: "200 /day",
pioneerValue: "500 /day",
icon: nil
)
]
var body: some View {
HStack(spacing: 0) {
//
featureNamesColumn
.frame(minWidth: 163)
// Free
planColumn(title: "Free", isPioneer: false)
.layoutPriority(1)
// Pioneer
planColumn(title: "Pioneer", isPioneer: true)
.frame(width: 88)
}
.background(Theme.Colors.cardBackground)
.cornerRadius(Theme.CornerRadius.medium)
.shadow(
color: Theme.Shadows.small,
radius: Theme.Shadows.cardShadow.radius,
x: Theme.Shadows.cardShadow.x,
y: Theme.Shadows.cardShadow.y
)
}
// MARK: -
private var featureNamesColumn: some View {
VStack(spacing: 0) {
//
Text("")
.font(Typography.font(for: .title, family: .quicksandBold, size: 14))
.padding(.vertical, Theme.Spacing.sm)
.frame(maxWidth: .infinity, minHeight: 30)
//
ForEach(Array(features.enumerated()), id: \.offset) { index, feature in
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
Text(feature.title)
.font(Typography.font(for: .body, family: .quicksandBold, size: 12))
.foregroundColor(Theme.Colors.textPrimary)
.multilineTextAlignment(.leading)
if let subtitle = feature.subtitle {
Text(subtitle)
.font(Typography.font(for: .caption, family: .quicksandRegular))
.foregroundColor(Theme.Colors.textSecondary)
.multilineTextAlignment(.leading)
}
}
.frame(maxWidth: .infinity, minHeight: 30, alignment: .leading)
.padding(.horizontal, Theme.Spacing.sm)
.padding(.vertical, Theme.Spacing.sm)
}
}
.padding(Theme.Spacing.sm)
}
// MARK: -
private func planColumn(title: String, isPioneer: Bool) -> some View {
VStack(spacing: 0) {
//
VStack(spacing: Theme.Spacing.xs) {
Text(title)
.font(Typography.font(for: .title, family: .quicksandBold, size: 14))
.foregroundColor(Color.black)
}
.frame(maxWidth: .infinity)
.padding(.vertical, Theme.Spacing.sm)
//
ForEach(Array(features.enumerated()), id: \.offset) { index, feature in
let value = isPioneer ? feature.pioneerValue : feature.freeValue
Text(value)
.font(Typography.font(for: .body, family: .quicksandRegular, size: 12))
.foregroundColor(isPioneer ? Color.black : Theme.Colors.textSecondary)
.fontWeight(isPioneer ? .semibold : .regular)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, minHeight: 30)
.padding(.vertical, Theme.Spacing.sm)
}
}
.frame(maxWidth: .infinity)
.background(isPioneer ? Theme.Colors.primaryLight : Color.white)
.cornerRadius(Theme.CornerRadius.medium)
.overlay(
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.stroke(
isPioneer ? Theme.Colors.primary : Theme.Colors.border,
lineWidth: isPioneer ? 1 : 0
)
)
.padding(Theme.Spacing.sm)
}
}
// MARK: -
#Preview("PlanCompare") {
ScrollView {
VStack(spacing: Theme.Spacing.xl) {
PlanCompare()
}
.padding()
}
.background(Theme.Colors.background)
}
#Preview("PlanCompare Dark") {
ScrollView {
VStack(spacing: Theme.Spacing.xl) {
PlanCompare()
}
.padding()
}
.background(Color.black)
.preferredColorScheme(.dark)
}

View File

@ -0,0 +1,135 @@
//
// PlanSelector.swift
// wake
//
// Created by fairclip on 2025/8/19.
//
import SwiftUI
// MARK: -
struct PlanSelector: View {
@Binding var selectedPlan: SubscriptionPlan?
let onPlanSelected: (SubscriptionPlan) -> Void
private let plans: [SubscriptionPlan] = [.free, .pioneer]
init(
selectedPlan: Binding<SubscriptionPlan?>,
onPlanSelected: @escaping (SubscriptionPlan) -> Void = { _ in }
) {
self._selectedPlan = selectedPlan
self.onPlanSelected = onPlanSelected
}
var body: some View {
HStack(spacing: Theme.Spacing.md) {
ForEach(plans, id: \.self) { plan in
PlanCard(
plan: plan,
isSelected: selectedPlan == plan,
onTap: {
selectedPlan = plan
onPlanSelected(plan)
}
)
}
}
}
}
// MARK: -
struct PlanCard: View {
let plan: SubscriptionPlan
let isSelected: Bool
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
ZStack {
//
VStack(spacing: Theme.Spacing.sm) {
// Popular
if plan.isPopular {
VStack {
HStack {
Spacer()
Text("Popular")
.font(Typography.font(for: .caption, family: .quicksandRegular))
.foregroundColor(Color.white)
.padding(.horizontal, Theme.Spacing.sm)
.padding(.vertical, Theme.Spacing.xs)
.background(Color.black)
.cornerRadius(Theme.CornerRadius.round, corners: [.bottomLeft])
}
Spacer()
VStack {
//
Text(plan.displayName)
.font(Typography.font(for: .title, family: .quicksandBold, size: 18))
.foregroundColor(plan == .pioneer ? Theme.Colors.textPrimary: Theme.Colors.textTertiary )
//
if plan == .pioneer {
Text(plan.price)
.font(Typography.font(for: .body, family: .quicksandBold, size: 20))
.foregroundColor(Theme.Colors.textPrimary)
}
}
Spacer()
Spacer()
}
}
else {
//
Text(plan.displayName)
.font(Typography.font(for: .title, family: .quicksandBold, size: 18))
.foregroundColor(plan == .pioneer ? Theme.Colors.textPrimary: Theme.Colors.textTertiary )
//
if plan == .pioneer {
Text(plan.price)
.font(Typography.font(for: .body, family: .quicksandBold, size: 20))
.foregroundColor(Theme.Colors.textPrimary)
}
}
}
.frame(maxWidth: .infinity)
.frame(height: 120)
.background(
plan == .pioneer ?
Theme.Colors.primary :
Theme.Colors.surface
)
.overlay(
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.stroke(
isSelected ? Theme.Colors.borderDark : Theme.Colors.border,
lineWidth: 2
)
)
.cornerRadius(Theme.CornerRadius.medium)
}
}
.buttonStyle(PlainButtonStyle())
}
}
// MARK: -
#Preview("Plan Selector") {
@State var selectedPlan: SubscriptionPlan? = .pioneer
VStack(spacing: 20) {
PlanSelector(
selectedPlan: $selectedPlan,
onPlanSelected: { plan in
print("Selected plan: \(plan.displayName)")
}
)
}
.padding()
.background(Color(.systemGroupedBackground))
}

View File

@ -0,0 +1,81 @@
import SwiftUI
// MARK: - Subscribe Button
struct SubscribeButton: View {
let title: String
let isLoading: Bool
let action: () -> Void
let subscribed: Bool
init(
title: String = "Subscribe",
isLoading: Bool,
subscribed: Bool,
action: @escaping () -> Void,
) {
self.title = title
self.isLoading = isLoading
self.action = action
self.subscribed = subscribed
}
var body: some View {
VStack(spacing: Theme.Spacing.xs) {
Button(action: {
guard !isLoading else { return }
action()
}) {
HStack(spacing: Theme.Spacing.sm) {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Theme.Colors.textInverse))
}
VStack {
if subscribed {
Spacer()
Text("Subscribed")
.font(Typography.font(for: .body, family: .quicksandBold))
Spacer()
}
else {
Spacer()
Spacer()
Text(title)
.font(Typography.font(for: .body, family: .quicksandBold))
Spacer()
// Fixed subtitle text as requested
Text("And get 5,000 Permanent Credits")
.font(Typography.font(for: .caption, family: .quicksandRegular))
.foregroundColor(Theme.Colors.textPrimary)
Spacer()
Spacer()
}
}
}
.frame(height: 56)
.frame(maxWidth: .infinity)
.background(Theme.Colors.primary) // primary color background
.clipShape(Capsule())
.shadow(
color: Theme.Shadows.buttonShadow.color,
radius: Theme.Shadows.buttonShadow.radius,
x: Theme.Shadows.buttonShadow.x,
y: Theme.Shadows.buttonShadow.y
)
}
.buttonStyle(.plain)
.disabled(isLoading || subscribed)
}
}
}
#Preview("SubscribeButton") {
VStack(spacing: Theme.Spacing.xl) {
SubscribeButton(isLoading: false, subscribed: false) {}
SubscribeButton(isLoading: true, subscribed: false) {}
SubscribeButton(isLoading: false, subscribed: true) {}
}
.padding()
.background(Theme.Colors.background)
}

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: 20) {
//
Text(status.title)
.font(Typography.font(for: .headline, family: .quicksandBold, size: 32))
.foregroundColor(status.textColor)
//
if case .pioneer(let expiryDate) = status {
VStack(alignment: .leading, spacing: 4) {
Text("Expires on :")
.font(Typography.font(for: .body, family: .quicksandRegular))
.foregroundColor(status.textColor.opacity(0.7))
Text(formatDate(expiryDate))
.font(Typography.font(for: .body, family: .quicksandRegular))
.foregroundColor(status.textColor)
}
} else {
Button(action: {
onSubscribeTap?()
}) {
Text("Subscribe")
.font(Typography.font(for: .title, family: .quicksandRegular, size: 16))
.foregroundColor(Theme.Colors.textPrimary)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Theme.Gradients.backgroundGradient)
.cornerRadius(Theme.CornerRadius.extraLarge)
}
}
}
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))
}

View File

@ -0,0 +1,264 @@
//
// SubscribeView.swift
// wake
//
// Created by fairclip on 2025/8/19.
//
import SwiftUI
import StoreKit
// 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 {
@Environment(\.dismiss) private var dismiss
@StateObject private var store = IAPManager()
@State private var selectedPlan: SubscriptionPlan? = .pioneer
@State private var isLoading = false
@State private var showErrorAlert = false
@State private var errorText = ""
//
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 {
VStack(spacing: 0) {
//
SimpleNaviHeader(title: "Subscription") {
dismiss()
}
ScrollView {
VStack(spacing: 0) {
//
currentSubscriptionCard
//
creditsSection
VStack {
//
subscriptionPlansSection
//
specialOfferBanner
}
.background(Theme.Colors.cardBackground)
.cornerRadius(Theme.CornerRadius.medium)
.padding(.horizontal, Theme.Spacing.lg)
.padding(.vertical, Theme.Spacing.lg)
//
featureComparisonTable
//
subscribeButton
//
legalLinks
Spacer(minLength: 100)
}
}
.background(Theme.Colors.background)
}
.navigationBarHidden(true)
.task {
// Load products and refresh current entitlements on appear
await store.loadProducts()
await store.refreshEntitlements()
}
.onChange(of: store.isPurchasing) { newValue in
// Bind purchasing state to button loading
isLoading = newValue
}
.onChange(of: store.errorMessage) { newValue in
if let message = newValue, !message.isEmpty {
errorText = message
showErrorAlert = true
}
}
.alert("Purchase Error", isPresented: $showErrorAlert) {
Button("OK", role: .cancel) { store.errorMessage = nil }
} message: {
Text(errorText)
}
}
// MARK: -
private var currentSubscriptionCard: some View {
let status: SubscriptionStatus = {
if store.isSubscribed {
return .pioneer(expiryDate: store.subscriptionExpiry ?? Date())
} else {
return .free
}
}()
return SubscriptionStatusBar(
status: status,
onSubscribeTap: {
//
handleSubscribe()
}
)
.padding(.horizontal, Theme.Spacing.xl)
}
// MARK: -
private var creditsSection: some View {
VStack(spacing: 16) {
CreditsInfoCard(
totalCredits: 3290,
onInfoTap: {
//
},
onDetailTap: {
//
}
)
}
.padding(.horizontal, Theme.Spacing.xl)
.padding(.top, Theme.Spacing.xl)
}
// MARK: -
private var subscriptionPlansSection: some View {
PlanSelector(
selectedPlan: $selectedPlan,
onPlanSelected: { plan in
print("Selected plan: \(plan.displayName)")
}
)
.padding(.horizontal, Theme.Spacing.xl)
.padding(.top, Theme.Spacing.xl)
}
// MARK: -
private var specialOfferBanner: some View {
HStack(spacing: 0) {
Text("First")
.font(Typography.font(for: .footnote, family: .quicksandRegular))
.foregroundColor(Theme.Colors.textPrimary)
Text(" 100")
.font(Typography.font(for: .footnote, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
Text(" users get a special deal: justs")
.font(Typography.font(for: .footnote, family: .quicksandRegular))
.foregroundColor(Theme.Colors.textPrimary)
Text(" $1")
.font(Typography.font(for: .footnote, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
Text(" for your first month!")
.font(Typography.font(for: .footnote, family: .quicksandRegular))
.foregroundColor(Theme.Colors.textPrimary)
}
.multilineTextAlignment(.center)
.padding(.horizontal, Theme.Spacing.lg)
.padding(.top, Theme.Spacing.sm)
.padding(.bottom, Theme.Spacing.lg)
}
// MARK: -
private var featureComparisonTable: some View {
PlanCompare()
.padding(.horizontal, Theme.Spacing.lg)
}
// MARK: -
private var subscribeButton: some View {
VStack(spacing: 12) {
SubscribeButton(
title: "Subscribe",
isLoading: isLoading,
subscribed: store.isSubscribed,
action: handleSubscribe
)
}
.padding(.horizontal, Theme.Spacing.xl)
.padding(.top, Theme.Spacing.lg)
}
// MARK: -
private var legalLinks: some View {
HStack(spacing: 8) {
Button(action: {
//
}) {
Text("Terms of Service")
.underline()
}
Text("|")
.foregroundColor(.secondary)
Button(action: {
//
}) {
Text("Privacy Policy")
.underline()
}
Text("|")
.foregroundColor(.secondary)
Button(action: {
Task { await store.restorePurchases() }
}) {
Text("Restore Purchase")
.underline()
}
}
.font(Typography.font(for: .caption, family: .quicksandRegular))
.foregroundColor(.secondary)
.padding(.top, Theme.Spacing.sm)
}
// MARK: -
private func handleSubscribe() {
Task { await store.purchasePioneer() }
}
}
#Preview {
SubscribeView()
}

View File

@ -0,0 +1,70 @@
import SwiftUI
import WebKit
struct SVGImage: UIViewRepresentable {
let svgName: String
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.isOpaque = false
webView.backgroundColor = .clear
webView.scrollView.isScrollEnabled = false
webView.scrollView.contentInsetAdjustmentBehavior = .never
if let url = Bundle.main.url(forResource: svgName, withExtension: "svg") {
let htmlString = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
svg {
width: 100%;
height: 100%;
display: block;
}
</style>
</head>
<body>
<div style="width:100%; height:100%; display:flex; align-items:center; justify-content:center;">
<img src="\(url.absoluteString)" style="max-width:100%; max-height:100%;" />
</div>
</body>
</html>
"""
webView.loadHTMLString(htmlString, baseURL: nil)
}
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {}
}
// MARK: - View Modifier for SVG Image
struct SVGImageModifier: ViewModifier {
let size: CGSize
func body(content: Content) -> some View {
content
.frame(width: size.width, height: size.height)
.aspectRatio(contentMode: .fit)
}
}
extension View {
func svgImageStyle(size: CGSize) -> some View {
self.modifier(SVGImageModifier(size: size))
}
}
// Usage:
// SVGImage(svgName: "Avatar")

View File

@ -1,14 +1,40 @@
//
// WakeApp.swift
// Wake
//
// Created by elliwood on 2025/8/11.
//
import SwiftUI
import UIKit
import SwiftData
@main
struct WakeApp: App {
// init() {
// //
// print("\n=== ===")
// for family in UIFont.familyNames.sorted() {
// print("\n\(family):")
// for name in UIFont.fontNames(forFamilyName: family).sorted() {
// print(" - \(name)")
// }
// }
// }
// 使
let container: ModelContainer
init() {
do {
// 1.
container = try ModelContainer(for: Login.self)
} catch {
// 2.
let url = URL.applicationSupportDirectory.appending(path: "default.store")
if FileManager.default.fileExists(atPath: url.path) {
try? FileManager.default.removeItem(at: url)
}
// 3.
container = try! ModelContainer(for: Login.self)
}
}
var body: some Scene {
WindowGroup {
ContentView()
@ -25,5 +51,7 @@ struct WakeApp: App {
// }
// }
}
//
.modelContainer(container)
}
}

10
wake/wake.entitlements Normal file
View File

@ -0,0 +1,10 @@
<?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>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
</dict>
</plist>