Compare commits
22 Commits
6ed7ab024b
...
f15f70cc8c
| Author | SHA1 | Date | |
|---|---|---|---|
| f15f70cc8c | |||
| 28a9db04ab | |||
| bae3923475 | |||
| 62c2defb1d | |||
| 7d40fe3203 | |||
| f361d73bf7 | |||
|
|
10e9324049 | ||
|
|
afa14f1fb2 | ||
| a225aa2788 | |||
|
|
119b671838 | ||
|
|
4f92f52bb7 | ||
|
|
cc305aa84a | ||
|
|
9e0a4a92c3 | ||
|
|
35e949cf17 | ||
|
|
ace3e3dd14 | ||
|
|
fc5735964f | ||
|
|
cd9af65019 | ||
|
|
a8f6be62e1 | ||
|
|
8eecd42076 | ||
|
|
50bff7cf12 | ||
|
|
095d95b420 | ||
|
|
35962c644e |
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"lldb.library": "/Library/Developer/CommandLineTools/Library/PrivateFrameworks/LLDB.framework/Versions/A/LLDB",
|
||||
"lldb.launch.expressions": "native"
|
||||
}
|
||||
@ -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 */
|
||||
|
||||
Binary file not shown.
BIN
wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate
generated
Normal file
BIN
wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate
generated
Normal file
Binary file not shown.
81
wake.xcodeproj/xcshareddata/xcschemes/wake.xcscheme
Normal file
81
wake.xcodeproj/xcshareddata/xcschemes/wake.xcscheme
Normal 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>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "55F37A93-4556-4005-B9BD-8F1A1D6A8474"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
</Bucket>
|
||||
@ -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
BIN
wake/.DS_Store
vendored
Binary file not shown.
16
wake/Assets/Svg/Avatar.svg
Normal file
16
wake/Assets/Svg/Avatar.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 184 KiB |
121
wake/Components/Buttons/ReturnButton.swift
Normal file
121
wake/Components/Buttons/ReturnButton.swift
Normal 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))
|
||||
}
|
||||
163
wake/Components/Navi/NaviHeader.swift
Normal file
163
wake/Components/Navi/NaviHeader.swift
Normal 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))
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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>
|
||||
34
wake/Extensions/ColorExtensions.swift
Normal file
34
wake/Extensions/ColorExtensions.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
22
wake/Extensions/ViewExtensions.swift
Normal file
22
wake/Extensions/ViewExtensions.swift
Normal 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
34
wake/Info.plist
Normal 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
215
wake/MemoWake.storekit
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
wake/Resources/Fonts/Quicksand-Bold.ttf
Normal file
BIN
wake/Resources/Fonts/Quicksand-Bold.ttf
Normal file
Binary file not shown.
BIN
wake/Resources/Fonts/Quicksand-Light.ttf
Normal file
BIN
wake/Resources/Fonts/Quicksand-Light.ttf
Normal file
Binary file not shown.
BIN
wake/Resources/Fonts/Quicksand-Medium.ttf
Normal file
BIN
wake/Resources/Fonts/Quicksand-Medium.ttf
Normal file
Binary file not shown.
BIN
wake/Resources/Fonts/Quicksand-Regular.ttf
Normal file
BIN
wake/Resources/Fonts/Quicksand-Regular.ttf
Normal file
Binary file not shown.
BIN
wake/Resources/Fonts/Quicksand-SemiBold.ttf
Normal file
BIN
wake/Resources/Fonts/Quicksand-SemiBold.ttf
Normal file
Binary file not shown.
BIN
wake/Resources/Quicksand x.ttf
Normal file
BIN
wake/Resources/Quicksand x.ttf
Normal file
Binary file not shown.
BIN
wake/Resources/SankeiCutePopanime.ttf
Normal file
BIN
wake/Resources/SankeiCutePopanime.ttf
Normal file
Binary file not shown.
22
wake/SwiftData/LoginModel.swift
Normal file
22
wake/SwiftData/LoginModel.swift
Normal 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
191
wake/Theme.swift
Normal 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
100
wake/Typography.swift
Normal 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
125
wake/Utils/IAPManager.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
122
wake/View/Components/Upload/Avatar.swift
Normal file
122
wake/View/Components/Upload/Avatar.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
135
wake/View/Components/UserProfileModal.swift
Normal file
135
wake/View/Components/UserProfileModal.swift
Normal 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))
|
||||
}
|
||||
290
wake/View/Credits/CreditsDetailView.swift
Normal file
290
wake/View/Credits/CreditsDetailView.swift
Normal 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()
|
||||
}
|
||||
107
wake/View/Credits/CreditsInfoCard.swift
Normal file
107
wake/View/Credits/CreditsInfoCard.swift
Normal 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))
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
153
wake/View/Owner/UserInfo/UserInfo.swift
Normal file
153
wake/View/Owner/UserInfo/UserInfo.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
165
wake/View/Subscribe/Components/PlanCompare.swift
Normal file
165
wake/View/Subscribe/Components/PlanCompare.swift
Normal 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)
|
||||
}
|
||||
135
wake/View/Subscribe/Components/PlanSelector.swift
Normal file
135
wake/View/Subscribe/Components/PlanSelector.swift
Normal 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))
|
||||
}
|
||||
81
wake/View/Subscribe/Components/SubscribeButton.swift
Normal file
81
wake/View/Subscribe/Components/SubscribeButton.swift
Normal 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)
|
||||
}
|
||||
140
wake/View/Subscribe/Components/SubscriptionStatusBar.swift
Normal file
140
wake/View/Subscribe/Components/SubscriptionStatusBar.swift
Normal 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))
|
||||
}
|
||||
264
wake/View/Subscribe/SubscribeView.swift
Normal file
264
wake/View/Subscribe/SubscribeView.swift
Normal 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()
|
||||
}
|
||||
70
wake/Views/Utils/SVGImage.swift
Normal file
70
wake/Views/Utils/SVGImage.swift
Normal 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")
|
||||
@ -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
10
wake/wake.entitlements
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user