diff --git a/ios/PushDeer-iOS/Podfile b/ios/PushDeer-iOS/Podfile index 69cda7e..5255130 100644 --- a/ios/PushDeer-iOS/Podfile +++ b/ios/PushDeer-iOS/Podfile @@ -7,6 +7,10 @@ use_frameworks! def commonPods # Pods for common pod 'Moya', '~> 15.0' + pod 'SDWebImageSwiftUI', '~> 2.0.2' + pod 'KRProgressHUD', '~> 3.4.7' + + # pod 'WoodPeckeriOS', :configurations => ['Debug'] end target 'PushDeer' do diff --git a/ios/PushDeer-iOS/Podfile.lock b/ios/PushDeer-iOS/Podfile.lock index f7de2c8..aeca252 100644 --- a/ios/PushDeer-iOS/Podfile.lock +++ b/ios/PushDeer-iOS/Podfile.lock @@ -1,22 +1,40 @@ PODS: - Alamofire (5.5.0) + - KRActivityIndicatorView (3.0.7) + - KRProgressHUD (3.4.7): + - KRActivityIndicatorView (= 3.0.7) - Moya (15.0.0): - Moya/Core (= 15.0.0) - Moya/Core (15.0.0): - Alamofire (~> 5.0) + - SDWebImage (5.12.1): + - SDWebImage/Core (= 5.12.1) + - SDWebImage/Core (5.12.1) + - SDWebImageSwiftUI (2.0.2): + - SDWebImage (~> 5.10) DEPENDENCIES: + - KRProgressHUD (~> 3.4.7) - Moya (~> 15.0) + - SDWebImageSwiftUI (~> 2.0.2) SPEC REPOS: trunk: - Alamofire + - KRActivityIndicatorView + - KRProgressHUD - Moya + - SDWebImage + - SDWebImageSwiftUI SPEC CHECKSUMS: Alamofire: 1c4fb5369c3fe93d2857c780d8bbe09f06f97e7c + KRActivityIndicatorView: ad69e89c4ce40c986cf580595be4829dcad0e35a + KRProgressHUD: a248f0bc6c9c2aed40a37b76e03ffecc7f85c887 Moya: 138f0573e53411fb3dc17016add0b748dfbd78ee + SDWebImage: 4dc3e42d9ec0c1028b960a33ac6b637bb432207b + SDWebImageSwiftUI: 8a3923c95108312b03a599ec1498754af55a6819 -PODFILE CHECKSUM: 71abdebce610eefe1a0299d0a0bd00a463b0a79c +PODFILE CHECKSUM: e462e86a9cce18b92c573f662ef405e7091cd912 COCOAPODS: 1.11.2 diff --git a/ios/PushDeer-iOS/PushDeer.xcodeproj/project.pbxproj b/ios/PushDeer-iOS/PushDeer.xcodeproj/project.pbxproj index 9255dd8..3fccecd 100644 --- a/ios/PushDeer-iOS/PushDeer.xcodeproj/project.pbxproj +++ b/ios/PushDeer-iOS/PushDeer.xcodeproj/project.pbxproj @@ -25,13 +25,16 @@ 52450F432784943F003652D8 /* HttpRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52450F412784943F003652D8 /* HttpRequest.swift */; }; 52483FC2277ED6D5003A100E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52483FC1277ED6D5003A100E /* AppDelegate.swift */; }; 52483FC3277ED6D5003A100E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52483FC1277ED6D5003A100E /* AppDelegate.swift */; }; + 5287FF52278ADC8C00249A0E /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5287FF51278ADC8C00249A0E /* MarkdownUI */; }; + 5287FF54278AEA5B00249A0E /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5287FF53278AEA5B00249A0E /* MarkdownUI */; }; + 5287FF59278B3AAE00249A0E /* HToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5287FF58278B3AAE00249A0E /* HToast.swift */; }; + 5287FF5A278B3AAE00249A0E /* HToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5287FF58278B3AAE00249A0E /* HToast.swift */; }; 5292F4F92776BC7900B9A7BB /* PushDeerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5292F4F82776BC7900B9A7BB /* PushDeerApp.swift */; }; 5292F4FB2776BC7900B9A7BB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5292F4FA2776BC7900B9A7BB /* ContentView.swift */; }; 5292F4FD2776BC7A00B9A7BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5292F4FC2776BC7A00B9A7BB /* Assets.xcassets */; }; 5292F5002776BC7A00B9A7BB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5292F4FF2776BC7A00B9A7BB /* Preview Assets.xcassets */; }; 52B8CF5F277DE660004CB680 /* AppleSignInButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B8CF5E277DE660004CB680 /* AppleSignInButton.swift */; }; 52B8CF67277E0B44004CB680 /* PushDeerClipApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B8CF66277E0B44004CB680 /* PushDeerClipApp.swift */; }; - 52B8CF69277E0B44004CB680 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B8CF68277E0B44004CB680 /* ContentView.swift */; }; 52B8CF6B277E0B46004CB680 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 52B8CF6A277E0B46004CB680 /* Assets.xcassets */; }; 52B8CF6E277E0B46004CB680 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 52B8CF6D277E0B46004CB680 /* Preview Assets.xcassets */; }; 52B8CF73277E0B46004CB680 /* PushDeerClip.app in Embed App Clips */ = {isa = PBXBuildFile; fileRef = 52B8CF64277E0B44004CB680 /* PushDeerClip.app */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -58,6 +61,7 @@ 52F0243F277737470071D861 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F0243E277737470071D861 /* LoginView.swift */; }; 52F2C223277961D7006F08DC /* SettingsItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F2C222277961D7006F08DC /* SettingsItemView.swift */; }; 52F40D2F277CA05600766C24 /* MessageItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F40D2E277CA05600766C24 /* MessageItemView.swift */; }; + 52FBA09427874879003308C2 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5292F4FA2776BC7900B9A7BB /* ContentView.swift */; }; 64B0C15E70CCC382B480F76E /* Pods_PushDeer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E380A18349DE4D26071E913E /* Pods_PushDeer.framework */; }; /* End PBXBuildFile section */ @@ -99,6 +103,7 @@ 52450F412784943F003652D8 /* HttpRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpRequest.swift; sourceTree = ""; }; 52450F442784A95D003652D8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 52483FC1277ED6D5003A100E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 5287FF58278B3AAE00249A0E /* HToast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HToast.swift; sourceTree = ""; }; 5292F4F52776BC7900B9A7BB /* PushDeer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PushDeer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5292F4F82776BC7900B9A7BB /* PushDeerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushDeerApp.swift; sourceTree = ""; }; 5292F4FA2776BC7900B9A7BB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -107,7 +112,6 @@ 52B8CF5E277DE660004CB680 /* AppleSignInButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleSignInButton.swift; sourceTree = ""; }; 52B8CF64277E0B44004CB680 /* PushDeerClip.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PushDeerClip.app; sourceTree = BUILT_PRODUCTS_DIR; }; 52B8CF66277E0B44004CB680 /* PushDeerClipApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushDeerClipApp.swift; sourceTree = ""; }; - 52B8CF68277E0B44004CB680 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 52B8CF6A277E0B46004CB680 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 52B8CF6D277E0B46004CB680 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 52B8CF6F277E0B46004CB680 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -134,6 +138,7 @@ buildActionMask = 2147483647; files = ( 64B0C15E70CCC382B480F76E /* Pods_PushDeer.framework in Frameworks */, + 5287FF52278ADC8C00249A0E /* MarkdownUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -142,6 +147,7 @@ buildActionMask = 2147483647; files = ( 4812F19BB0BFEFE089BC253E /* Pods_PushDeerClip.framework in Frameworks */, + 5287FF54278AEA5B00249A0E /* MarkdownUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -227,6 +233,7 @@ 52B8CF5D277DE5FF004CB680 /* Common */ = { isa = PBXGroup; children = ( + 5287FF58278B3AAE00249A0E /* HToast.swift */, 52B8CF5E277DE660004CB680 /* AppleSignInButton.swift */, ); path = Common; @@ -236,7 +243,6 @@ isa = PBXGroup; children = ( 52B8CF66277E0B44004CB680 /* PushDeerClipApp.swift */, - 52B8CF68277E0B44004CB680 /* ContentView.swift */, 52B8CF6A277E0B46004CB680 /* Assets.xcassets */, 52B8CF6F277E0B46004CB680 /* Info.plist */, 52B8CF70277E0B46004CB680 /* PushDeerClip.entitlements */, @@ -311,6 +317,9 @@ 52B8CF72277E0B46004CB680 /* PBXTargetDependency */, ); name = PushDeer; + packageProductDependencies = ( + 5287FF51278ADC8C00249A0E /* MarkdownUI */, + ); productName = PushDeer; productReference = 5292F4F52776BC7900B9A7BB /* PushDeer.app */; productType = "com.apple.product-type.application"; @@ -330,6 +339,9 @@ dependencies = ( ); name = PushDeerClip; + packageProductDependencies = ( + 5287FF53278AEA5B00249A0E /* MarkdownUI */, + ); productName = PushDeerClip; productReference = 52B8CF64277E0B44004CB680 /* PushDeerClip.app */; productType = "com.apple.product-type.application.on-demand-install-capable"; @@ -361,6 +373,9 @@ Base, ); mainGroup = 5292F4EC2776BC7900B9A7BB; + packageReferences = ( + 5287FF50278ADC8C00249A0E /* XCRemoteSwiftPackageReference "MarkdownUI" */, + ); productRefGroup = 5292F4F62776BC7900B9A7BB /* Products */; projectDirPath = ""; projectRoot = ""; @@ -490,6 +505,7 @@ 52B8CF5F277DE660004CB680 /* AppleSignInButton.swift in Sources */, 52163EB92777417900594190 /* KeyListView.swift in Sources */, 5292F4FB2776BC7900B9A7BB /* ContentView.swift in Sources */, + 5287FF59278B3AAE00249A0E /* HToast.swift in Sources */, 52450F3B278491F8003652D8 /* AppState.swift in Sources */, 5292F4F92776BC7900B9A7BB /* PushDeerApp.swift in Sources */, 52483FC2277ED6D5003A100E /* AppDelegate.swift in Sources */, @@ -509,7 +525,6 @@ buildActionMask = 2147483647; files = ( 52B8CF82277E0C06004CB680 /* AppleSignInButton.swift in Sources */, - 52B8CF69277E0B44004CB680 /* ContentView.swift in Sources */, 52B8CF78277E0BF1004CB680 /* MainView.swift in Sources */, 52B8CF79277E0BFB004CB680 /* DeviceListView.swift in Sources */, 52B8CF84277E0C12004CB680 /* CardView.swift in Sources */, @@ -520,11 +535,13 @@ 52B8CF81277E0BFB004CB680 /* LoginView.swift in Sources */, 52B8CF7F277E0BFB004CB680 /* SettingsItemView.swift in Sources */, 52450F3C278491F8003652D8 /* AppState.swift in Sources */, + 5287FF5A278B3AAE00249A0E /* HToast.swift in Sources */, 52B8CF80277E0BFB004CB680 /* MessageItemView.swift in Sources */, 52483FC3277ED6D5003A100E /* AppDelegate.swift in Sources */, 52450F3927848243003652D8 /* PushDeerApi.swift in Sources */, 52B8CF86277E0C12004CB680 /* BaseNavigationView.swift in Sources */, 52B8CF67277E0B44004CB680 /* PushDeerClipApp.swift in Sources */, + 52FBA09427874879003308C2 /* ContentView.swift in Sources */, 52B8CF85277E0C12004CB680 /* Line.swift in Sources */, 52450F402784923D003652D8 /* Result.swift in Sources */, 52450F432784943F003652D8 /* HttpRequest.swift in Sources */, @@ -739,6 +756,10 @@ DEVELOPMENT_ASSET_PATHS = "\"PushDeerClip/Preview Content\""; DEVELOPMENT_TEAM = Y47WTLML2S; ENABLE_PREVIEWS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "APPCLIP=1", + ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PushDeerClip/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = PushDeer; @@ -753,6 +774,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; + OTHER_SWIFT_FLAGS = "$(inherited) -D APPCLIP"; PRODUCT_BUNDLE_IDENTIFIER = com.wskfz.pushdeer.ios.Clip; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -773,6 +795,10 @@ DEVELOPMENT_ASSET_PATHS = "\"PushDeerClip/Preview Content\""; DEVELOPMENT_TEAM = Y47WTLML2S; ENABLE_PREVIEWS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "APPCLIP=1", + ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PushDeerClip/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = PushDeer; @@ -787,6 +813,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; + OTHER_SWIFT_FLAGS = "$(inherited) -D APPCLIP"; PRODUCT_BUNDLE_IDENTIFIER = com.wskfz.pushdeer.ios.Clip; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -826,6 +853,30 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 5287FF50278ADC8C00249A0E /* XCRemoteSwiftPackageReference "MarkdownUI" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/gonzalezreal/MarkdownUI"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.5.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 5287FF51278ADC8C00249A0E /* MarkdownUI */ = { + isa = XCSwiftPackageProductDependency; + package = 5287FF50278ADC8C00249A0E /* XCRemoteSwiftPackageReference "MarkdownUI" */; + productName = MarkdownUI; + }; + 5287FF53278AEA5B00249A0E /* MarkdownUI */ = { + isa = XCSwiftPackageProductDependency; + package = 5287FF50278ADC8C00249A0E /* XCRemoteSwiftPackageReference "MarkdownUI" */; + productName = MarkdownUI; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 5292F4ED2776BC7900B9A7BB /* Project object */; } diff --git a/ios/PushDeer-iOS/PushDeer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/PushDeer-iOS/PushDeer.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..4879aa4 --- /dev/null +++ b/ios/PushDeer-iOS/PushDeer.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,70 @@ +{ + "object": { + "pins": [ + { + "package": "AttributedText", + "repositoryURL": "https://github.com/gonzalezreal/AttributedText", + "state": { + "branch": null, + "revision": "c345033e22d5a1cd0e9fe0ec405cc809a8349586", + "version": "0.3.1" + } + }, + { + "package": "combine-schedulers", + "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", + "state": { + "branch": null, + "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", + "version": "0.5.3" + } + }, + { + "package": "MarkdownUI", + "repositoryURL": "https://github.com/gonzalezreal/MarkdownUI", + "state": { + "branch": null, + "revision": "29d94710545952dd4c724cc2ca901848eef54ded", + "version": "0.5.2" + } + }, + { + "package": "NetworkImage", + "repositoryURL": "https://github.com/gonzalezreal/NetworkImage", + "state": { + "branch": null, + "revision": "04167e81ed89a14b5da9b856ead306b969bb5abd", + "version": "3.1.2" + } + }, + { + "package": "cmark", + "repositoryURL": "https://github.com/SwiftDocOrg/swift-cmark.git", + "state": { + "branch": null, + "revision": "9c8096a23f44794bde297452d87c455fc4f76d42", + "version": "0.29.0+20210102.9c8096a" + } + }, + { + "package": "SwiftCommonMark", + "repositoryURL": "https://github.com/gonzalezreal/SwiftCommonMark", + "state": { + "branch": null, + "revision": "ed60da54305c244d0f77bc8d08495e04161e96a4", + "version": "0.1.2" + } + }, + { + "package": "xctest-dynamic-overlay", + "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state": { + "branch": null, + "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", + "version": "0.2.1" + } + } + ] + }, + "version": 1 +} diff --git a/ios/PushDeer-iOS/PushDeer/AppDelegate.swift b/ios/PushDeer-iOS/PushDeer/AppDelegate.swift index 015d238..1a44355 100644 --- a/ios/PushDeer-iOS/PushDeer/AppDelegate.swift +++ b/ios/PushDeer-iOS/PushDeer/AppDelegate.swift @@ -9,9 +9,7 @@ import UIKit import UserNotifications class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { - - static var deviceToken: String = "" - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { let center = UNUserNotificationCenter.current() @@ -23,9 +21,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele Task { // APP启动后要先调一个无用接口, 用于触发国行手机的网络授权弹框, 未授权前调的接口会直接失败. (提前触发网络授权弹窗) - let result = try await HttpRequest.fake() - AppState.shared.token = result.token - HttpRequest.getDevices() + _ = try await HttpRequest.fake() } return true @@ -34,7 +30,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let deviceTokenString = deviceToken.reduce("", {$0 + String(format: "%02X", $1)}) print("deviceToken: ", deviceTokenString) - AppDelegate.deviceToken = deviceTokenString; + AppState.shared.deviceToken = deviceTokenString } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { diff --git a/ios/PushDeer-iOS/PushDeer/Common/AppleSignInButton.swift b/ios/PushDeer-iOS/PushDeer/Common/AppleSignInButton.swift index 9c2c50d..b944187 100644 --- a/ios/PushDeer-iOS/PushDeer/Common/AppleSignInButton.swift +++ b/ios/PushDeer-iOS/PushDeer/Common/AppleSignInButton.swift @@ -13,12 +13,12 @@ import AuthenticationServices /// 封装一个 Apple 登录按钮, 模仿 SwiftUI 中 SignInWithAppleButton 的行为 /// /// 那么为什么不直接使用系统的呢? 问的好, SignInWithAppleButton 在 macOS 上点击会报错, 在 iOS 上倒是正常. -struct AppleSignInButton: UIViewRepresentable { +@MainActor struct AppleSignInButton: UIViewRepresentable { var type = ASAuthorizationAppleIDButton.ButtonType.signIn var style = ASAuthorizationAppleIDButton.Style.black let onRequest: (ASAuthorizationAppleIDRequest) -> Void - let onCompletion: (Result) -> Void + let onCompletion: (Result) async -> Void func makeUIView(context: Context) -> ASAuthorizationAppleIDButton { let signInButton = ASAuthorizationAppleIDButton(type: type, style: style) @@ -40,11 +40,11 @@ struct AppleSignInButton: UIViewRepresentable { class AppleSignInCoordinator: NSObject, ASAuthorizationControllerDelegate { let onRequest: (ASAuthorizationAppleIDRequest) -> Void - let onCompletion: (Result) -> Void + let onCompletion: (Result) async -> Void init( onRequest: @escaping (ASAuthorizationAppleIDRequest) -> Void, - onCompletion: @escaping (Result) -> Void + onCompletion: @escaping (Result) async -> Void ) { self.onRequest = onRequest self.onCompletion = onCompletion @@ -61,11 +61,15 @@ class AppleSignInCoordinator: NSObject, ASAuthorizationControllerDelegate { func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { print(authorization.debugDescription) - onCompletion(.success(authorization)) + Task { + await onCompletion(.success(authorization)) + } } func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { print(error.localizedDescription) - onCompletion(.failure(error)) + Task { + await onCompletion(.failure(error)) + } } } diff --git a/ios/PushDeer-iOS/PushDeer/Common/HToast.swift b/ios/PushDeer-iOS/PushDeer/Common/HToast.swift new file mode 100644 index 0000000..ac5e0f1 --- /dev/null +++ b/ios/PushDeer-iOS/PushDeer/Common/HToast.swift @@ -0,0 +1,29 @@ +// +// HToast.swift +// LotusDesktop +// +// Created by HEXT on 2020/9/26. +// Copyright © 2020 HEXT. All rights reserved. +// + +import UIKit +import KRProgressHUD + +struct HToast { + static func showText(_ msg: String) { + KRProgressHUD.showMessage(msg) + } + static func showSuccess(_ msg: String?) { + KRProgressHUD.showSuccess(withMessage: msg) + } + static func showError(_ msg: String?) { + KRProgressHUD.showError(withMessage: msg) + } + + static func showLoading(_ msg: String?) { + KRProgressHUD.show(withMessage: msg) + } + static func dismiss() { + KRProgressHUD.dismiss() + } +} diff --git a/ios/PushDeer-iOS/PushDeer/ContentView.swift b/ios/PushDeer-iOS/PushDeer/ContentView.swift index 13b6c27..078c67f 100644 --- a/ios/PushDeer-iOS/PushDeer/ContentView.swift +++ b/ios/PushDeer-iOS/PushDeer/ContentView.swift @@ -8,9 +8,15 @@ import SwiftUI struct ContentView: View { + + @EnvironmentObject private var store: AppState + var body: some View { -// LoginView() - MainView() + if store.token.isEmpty { + LoginView() + } else { + MainView() + } } } diff --git a/ios/PushDeer-iOS/PushDeer/Info.plist b/ios/PushDeer-iOS/PushDeer/Info.plist index fde08b1..6ddad4c 100644 --- a/ios/PushDeer-iOS/PushDeer/Info.plist +++ b/ios/PushDeer-iOS/PushDeer/Info.plist @@ -2,14 +2,24 @@ + NSPhotoLibraryAddUsageDescription + APP为您提供保存图片到相册的功能 + CFBundleAllowMixedLocalizations + NSAppTransportSecurity NSAllowsArbitraryLoads + NSBonjourServices + + _adhp._tcp + UIBackgroundModes remote-notification + NSLocalNetworkUsageDescription + APP需要访问本地网络以供调试(仅在开发时) diff --git a/ios/PushDeer-iOS/PushDeer/Model/Result.swift b/ios/PushDeer-iOS/PushDeer/Model/Result.swift index 3e8deb6..1107d68 100644 --- a/ios/PushDeer-iOS/PushDeer/Model/Result.swift +++ b/ios/PushDeer-iOS/PushDeer/Model/Result.swift @@ -20,6 +20,17 @@ struct TokenContent: Codable{ let token: String } +struct UserInfoContent: Codable{ + let id: Int + let name: String + let email: String + let apple_id: String + let wechat_id: String? + let level: Int + let created_at: String + let updated_at: String +} + struct DeviceItem: Codable, Identifiable{ let id: Int let uid: String @@ -39,7 +50,10 @@ struct KeyContent: Codable{ struct KeyItem: Codable, Identifiable{ let id: Int + let name: String + let uid: String let key: String + let created_at: String } struct MessageContent: Codable{ @@ -52,8 +66,37 @@ struct MessageItem: Codable, Identifiable{ let text: String let desp: String let type: String + let created_at: String } struct ActionContent: Codable{ let message: String } + +struct PushResultContent: Codable{ + let result: Array +} + +let dateFormatter = DateFormatter() + +extension KeyItem { + var createdDateStr: String { + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ" + let createdDate = dateFormatter.date(from: self.created_at) + dateFormatter.dateFormat = "yyyy/MM/dd" + return dateFormatter.string(from: createdDate ?? Date()) + } +} + +extension MessageItem { + var createdDateStr: String { + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ" + let createdDate = dateFormatter.date(from: self.created_at) ?? Date() + if Calendar.current.isDateInToday(createdDate) { + dateFormatter.dateFormat = "HH:mm:ss" + } else { + dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss" + } + return dateFormatter.string(from: createdDate) + } +} diff --git a/ios/PushDeer-iOS/PushDeer/Service/AppState.swift b/ios/PushDeer-iOS/PushDeer/Service/AppState.swift index 24476bb..0d22db1 100644 --- a/ios/PushDeer-iOS/PushDeer/Service/AppState.swift +++ b/ios/PushDeer-iOS/PushDeer/Service/AppState.swift @@ -6,14 +6,86 @@ // import Foundation +import AuthenticationServices + class AppState: ObservableObject { - @Published var token : String? = nil + /// 账号 token + @Published var token : String { + didSet { + UserDefaults.standard.set(token, forKey: "PushDeer_token") + } + } + /// 设备列表 @Published var devices: [DeviceItem] = [] + /// key 列表 @Published var keys: [KeyItem] = [] + /// 消息列表 @Published var messages: [MessageItem] = [] - @Published var tab_selected: Int = 1 - @Published var device_token: String = "" + /// 选中的 tab 下标 + @Published var tabSelectedIndex: Int { + didSet { + UserDefaults.standard.set(tabSelectedIndex, forKey: "PushDeer_tabSelectedIndex") + } + } + /// 设备推送 token + @Published var deviceToken: String = "" + /// 用户信息 + @Published var userInfo: UserInfoContent? + /// 是否显示测试发推送的 UI + @Published var isShowTestPush: Bool { + didSet { + UserDefaults.standard.set(isShowTestPush, forKey: "PushDeer_isShowTestPush") + } + } + + var isAppClip: Bool { +#if APPCLIP + return true +#else + return false +#endif + } + static let shared = AppState() - private init() {} + private init() { + let _token = UserDefaults.standard.string(forKey: "PushDeer_token") + let _tabSelectedIndex = UserDefaults.standard.integer(forKey: "PushDeer_tabSelectedIndex") + let _isShowTestPush = UserDefaults.standard.object(forKey: "PushDeer_isShowTestPush") + token = _token ?? "" + tabSelectedIndex = _tabSelectedIndex + isShowTestPush = _isShowTestPush as? Bool ?? true + } + + func appleIdLogin(_ result: Result) async throws -> TokenContent { + switch result { + case let .success(authorization): + if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { + // 用户唯一ID,在一个开发者账号下的APP获取到的是一样的 + print(appleIDCredential.user) // 000791.7a323f1326dd4674bc16d32fd6339875.1424 + // 注意:当第一次认证成功之后,将不会再返回email,fullName等信息,可以在设置->Apple ID->密码与安全性->使用您AppleID的App 中删除对应的APP。 + print(appleIDCredential.email as Any) // easychen@qq.com + print(appleIDCredential.fullName as Any) // givenName: lijie familyName: chen + // 「JWT」格式的token,用于验证信息合法性。其值用.分割成3段, 中间一段base64后会看到包含了 用户唯一标识 和 邮箱 等字段 + let idToken = String(data:appleIDCredential.identityToken!, encoding: .utf8) + print(idToken as Any) + + do { + // 请求接口 + let result = try await HttpRequest.login(idToken: idToken!) + print(result) + // 登录成功 + return result + + } catch { + print(error) + } + } + case let .failure(error): + print(error) + } + // 登录失败 + throw NSError(domain: "登录失败", code: -1, userInfo: nil) + } + } diff --git a/ios/PushDeer-iOS/PushDeer/Service/HttpRequest.swift b/ios/PushDeer-iOS/PushDeer/Service/HttpRequest.swift index d498a83..ccf6b75 100644 --- a/ios/PushDeer-iOS/PushDeer/Service/HttpRequest.swift +++ b/ios/PushDeer-iOS/PushDeer/Service/HttpRequest.swift @@ -8,6 +8,7 @@ import Foundation import Moya +@MainActor struct HttpRequest { static let provider = MoyaProvider(callbackQueue: DispatchQueue.main) @@ -23,6 +24,9 @@ struct HttpRequest { print(result) if let content = result.content, result.code == 0 { continuation.resume(returning: content) + } else if result.code == 80403 { + AppState.shared.token = "" + continuation.resume(throwing: NSError(domain: result.error ?? "接口报错", code: result.code, userInfo: nil)) } else { continuation.resume(throwing: NSError(domain: result.error ?? "接口报错", code: result.code, userInfo: nil)) } @@ -46,10 +50,68 @@ struct HttpRequest { return try await request(.login(idToken: idToken), resultType: TokenContent.self) } - @MainActor static func getDevices() { + static func getUserInfo() async throws -> UserInfoContent { + return try await request(.getUserInfo(token: AppState.shared.token), resultType: UserInfoContent.self) + } + + static func regDevice() async throws -> DeviceContent { + return try await request(.regDevice( + token: AppState.shared.token, + name: UIDevice.current.name, + device_id: AppState.shared.deviceToken, + is_clip: AppState.shared.isAppClip ? 1 : 0 + ), resultType: DeviceContent.self) + } + + static func rmDevice(id: Int) async throws -> ActionContent { + return try await request(.rmDevice(token: AppState.shared.token, id: id), resultType: ActionContent.self) + } + + static func getDevices() async throws -> DeviceContent { + return try await request(.getDevices(token: AppState.shared.token), resultType: DeviceContent.self) + } + static func loadDevices() { _Concurrency.Task { - let result = try await request(.getDevices(token: AppState.shared.token ?? ""), resultType: DeviceContent.self) + let result = try await getDevices() AppState.shared.devices = result.devices } } + + static func genKey() async throws -> KeyContent { + return try await request(.genKey(token: AppState.shared.token), resultType: KeyContent.self) + } + + static func regenKey(id: Int) async throws -> ActionContent { + return try await request(.regenKey(token: AppState.shared.token, id: id), resultType: ActionContent.self) + } + + static func renameKey(id: Int, name: String) async throws -> ActionContent { + return try await request(.renameKey(token: AppState.shared.token, id: id, name: name), resultType: ActionContent.self) + } + + static func rmKey(id: Int) async throws -> ActionContent { + return try await request(.rmKey(token: AppState.shared.token, id: id), resultType: ActionContent.self) + } + + static func getKeys() async throws -> KeyContent { + return try await request(.getKeys(token: AppState.shared.token), resultType: KeyContent.self) + } + static func loadKeys() { + _Concurrency.Task { + let result = try await getKeys() + AppState.shared.keys = result.keys + } + } + + static func push(pushkey: String, text: String, desp: String, type: String) async throws -> PushResultContent { + return try await request(.push(pushkey: pushkey, text: text, desp: desp, type: type), resultType: PushResultContent.self) + } + + static func getMessages() async throws -> MessageContent { + return try await request(.getMessages(token: AppState.shared.token, limit: 100), resultType: MessageContent.self) + } + + static func rmMessage(id: Int) async throws -> ActionContent { + return try await request(.rmMessage(token: AppState.shared.token, id: id), resultType: ActionContent.self) + } } diff --git a/ios/PushDeer-iOS/PushDeer/Service/PushDeerApi.swift b/ios/PushDeer-iOS/PushDeer/Service/PushDeerApi.swift index 11fc662..6cfa3b8 100644 --- a/ios/PushDeer-iOS/PushDeer/Service/PushDeerApi.swift +++ b/ios/PushDeer-iOS/PushDeer/Service/PushDeerApi.swift @@ -12,6 +12,7 @@ enum PushDeerApi { case fake case login(idToken: String) + case getUserInfo(token: String) case regDevice(token: String, name: String, device_id: String, is_clip: Int) case getDevices(token: String) @@ -39,6 +40,8 @@ extension PushDeerApi: TargetType { return "/login/fake" case .login: return "/login/idtoken" + case .getUserInfo: + return "/user/info" case .regDevice: return "/device/reg" @@ -79,6 +82,8 @@ extension PushDeerApi: TargetType { return .requestParameters(parameters: [:], encoding: URLEncoding.queryString) case let .login(idToken): return .requestParameters(parameters: ["idToken": idToken], encoding: URLEncoding.queryString) + case let .getUserInfo(token): + return .requestParameters(parameters: ["token": token], encoding: URLEncoding.queryString) case let .regDevice(token, name, device_id, is_clip): return .requestParameters(parameters: ["token": token,"name": name, "device_id": device_id,"is_clip": is_clip], encoding: URLEncoding.queryString) diff --git a/ios/PushDeer-iOS/PushDeer/View/DeviceItemView.swift b/ios/PushDeer-iOS/PushDeer/View/DeviceItemView.swift index fe7dfff..70a013f 100644 --- a/ios/PushDeer-iOS/PushDeer/View/DeviceItemView.swift +++ b/ios/PushDeer-iOS/PushDeer/View/DeviceItemView.swift @@ -13,7 +13,7 @@ struct DeviceItemView: View { var body: some View { CardView { HStack{ - Image(systemName: "ipad.and.iphone") + Image(systemName: getSystemName(deviceName: name)) .resizable() .scaledToFit() .frame(width: 40, height: 40, alignment: .center) @@ -25,6 +25,37 @@ struct DeviceItemView: View { .frame(height: 80) } } + + func getSystemName(deviceName: String) -> String { + var deviceName = deviceName.lowercased() + deviceName = deviceName.replacingOccurrences(of: " ", with: "") + +// if deviceName.contains("clip") { +// return "appclip" +// } + if deviceName.contains("iphone") { + return "iphone" + } + if deviceName.contains("ipad") { + return "ipad.landscape" + } + if deviceName.contains("macbook") { + return "laptopcomputer" + } + if deviceName.contains("imac") { + return "desktopcomputer" + } + if deviceName.contains("macpro") { + return "macpro.gen3" + } + if deviceName.contains("macmini") { + return "macmini" + } + if deviceName.contains("mac") { + return "macwindow" + } + return "ipad.and.iphone" + } } struct DeviceItemView_Previews: PreviewProvider { diff --git a/ios/PushDeer-iOS/PushDeer/View/DeviceListView.swift b/ios/PushDeer-iOS/PushDeer/View/DeviceListView.swift index 64dbed9..080e76a 100644 --- a/ios/PushDeer-iOS/PushDeer/View/DeviceListView.swift +++ b/ios/PushDeer-iOS/PushDeer/View/DeviceListView.swift @@ -14,13 +14,21 @@ struct DeviceListView: View { BaseNavigationView(title: "设备") { ScrollView { LazyVStack(alignment: .center) { - ForEach(store.devices, id: \.id) { deviceItem in + ForEach(store.devices.reversed()) { deviceItem in DeletableView(contentView: { - DeviceItemView(name: deviceItem.name) + DeviceItemView(name: getName(deviceItem: deviceItem)) }, deleteAction: { store.devices.removeAll { _deviceItem in _deviceItem.id == deviceItem.id } + HToast.showSuccess("已删除") + Task { + do { + _ = try await HttpRequest.rmDevice(id: deviceItem.id) + } catch { + + } + } }) .padding(EdgeInsets(top: 18, leading: 26, bottom: 0, trailing: 24)) } @@ -28,8 +36,12 @@ struct DeviceListView: View { } } .navigationBarItems(trailing: Button(action: { - withAnimation(.easeOut) { -// store.devices.insert(DeviceItem(), at: 0) + Task { + let devices = try await HttpRequest.regDevice().devices + withAnimation(.easeOut) { + store.devices = devices + } + HToast.showSuccess("已添加当前设备") } }, label: { Image(systemName: "plus") @@ -37,13 +49,24 @@ struct DeviceListView: View { })) } .onAppear { - HttpRequest.getDevices() + HttpRequest.loadDevices() } } + + func getName(deviceItem: DeviceItem) -> String { + var name = deviceItem.name + if deviceItem.is_clip == 1 { + name += " [Clip]" + } + if deviceItem.device_id == store.deviceToken { + name += " (当前设备)" + } + return name + } } struct DeviceView_Previews: PreviewProvider { static var previews: some View { - DeviceListView() + DeviceListView().environmentObject(AppState.shared) } } diff --git a/ios/PushDeer-iOS/PushDeer/View/KeyItemView.swift b/ios/PushDeer-iOS/PushDeer/View/KeyItemView.swift index 9245cf5..85ef4c6 100644 --- a/ios/PushDeer-iOS/PushDeer/View/KeyItemView.swift +++ b/ios/PushDeer-iOS/PushDeer/View/KeyItemView.swift @@ -7,6 +7,57 @@ import SwiftUI +struct KeyItemTextField: View { + let keyItem: KeyItem + @State private var value = "" + @EnvironmentObject private var store: AppState + + init(keyItem: KeyItem) { + self.keyItem = keyItem + self._value = State(initialValue: keyItem.name) + } + + func textField() -> some View { + TextField("输入key名称", text: $value, onCommit: { + Task { + do { + // 调用接口修改 + _ = try await HttpRequest.renameKey(id: keyItem.id, name: value) + HToast.showSuccess("已修改key名称") + // 在此 keyItem 在列表中的下标 + let index = store.keys.firstIndex { _keyItem in + _keyItem.id == keyItem.id + } + if let index = index { + let _keyItem = store.keys[index] + // 更新列表中相应的 keyItem + store.keys[index] = KeyItem( + id: _keyItem.id, + name: value, + uid: _keyItem.uid, + key: _keyItem.key, + created_at: _keyItem.created_at + ) + } + } catch { + + } + } + }) + .font(.system(size: 20)) + .foregroundColor(Color.accentColor) + } + + var body: some View { + if #available(iOS 15.0, *) { + textField() + .submitLabel(.done) + } else { + textField() + } + } +} + /// 每个 Key 项的 View struct KeyItemView: View { let keyItem: KeyItem @@ -18,14 +69,12 @@ struct KeyItemView: View { .resizable() .scaledToFit() .frame(width: 38, height: 38) - Text("Key \(keyItem.id)") - .font(.system(size: 20)) - .foregroundColor(Color.accentColor) + KeyItemTextField(keyItem: keyItem) Spacer() Image(systemName: "calendar") .font(.system(size: 14)) .foregroundColor(Color.gray) - Text("2021/12/01") + Text(keyItem.createdDateStr) .font(.system(size: 14)) .foregroundColor(Color.gray) } @@ -42,6 +91,15 @@ struct KeyItemView: View { HStack { Button("重置") { print("点击重置") + Task { + do { + _ = try await HttpRequest.regenKey(id: keyItem.id) + HttpRequest.loadKeys() + HToast.showSuccess("已重置") + } catch { + + } + } } .font(.system(size: 20)) .frame(width: 90, height: 42) @@ -53,6 +111,7 @@ struct KeyItemView: View { Button("复制") { print("点击复制") UIPasteboard.general.string = keyItem.key + HToast.showSuccess("已复制") } .font(.system(size: 20)) .frame(width: 90, height: 42) @@ -67,6 +126,6 @@ struct KeyItemView: View { struct KeyItemView_Previews: PreviewProvider { static var previews: some View { - KeyItemView(keyItem: KeyItem(id: 1, key: "Key")) + KeyItemView(keyItem: KeyItem(id: 1, name: "name", uid: "1", key: "Key", created_at: "1111")) } } diff --git a/ios/PushDeer-iOS/PushDeer/View/KeyListView.swift b/ios/PushDeer-iOS/PushDeer/View/KeyListView.swift index 04ed228..70c5741 100644 --- a/ios/PushDeer-iOS/PushDeer/View/KeyListView.swift +++ b/ios/PushDeer-iOS/PushDeer/View/KeyListView.swift @@ -9,26 +9,29 @@ import SwiftUI /// Key 界面 struct KeyListView: View { - @State private var keyItems = [ - KeyItem(id: 1, key: UUID().uuidString), - KeyItem(id: 2, key: UUID().uuidString), - KeyItem(id: 3, key: UUID().uuidString), - KeyItem(id: 4, key: UUID().uuidString), - ] + @EnvironmentObject private var store: AppState var body: some View { BaseNavigationView(title: "Key") { ScrollView { LazyVStack(alignment: .center) { - ForEach(keyItems) { keyItem in + ForEach(store.keys.reversed()) { keyItem in DeletableView(contentView: { CardView { KeyItemView(keyItem: keyItem) } }, deleteAction: { - keyItems.removeAll { _keyItem in + store.keys.removeAll { _keyItem in keyItem.id == _keyItem.id } + HToast.showSuccess("已删除") + Task { + do { + _ = try await HttpRequest.rmKey(id: keyItem.id) + } catch { + + } + } }) .padding(EdgeInsets(top: 18, leading: 26, bottom: 0, trailing: 24)) } @@ -36,20 +39,26 @@ struct KeyListView: View { } } .navigationBarItems(trailing: Button(action: { - let keyItem = KeyItem(id: Int(arc4random_uniform(1000)), key: UUID().uuidString) - withAnimation(.easeOut) { - keyItems.insert(keyItem, at: 0) + Task { + let keys = try await HttpRequest.genKey().keys + withAnimation(.easeOut) { + store.keys = keys + } + HToast.showSuccess("已添加新Key") } }, label: { Image(systemName: "plus") .foregroundColor(Color(UIColor.lightGray)) })) } + .onAppear { + HttpRequest.loadKeys() + } } } struct KeyView_Previews: PreviewProvider { static var previews: some View { - KeyListView() + KeyListView().environmentObject(AppState.shared) } } diff --git a/ios/PushDeer-iOS/PushDeer/View/LoginView.swift b/ios/PushDeer-iOS/PushDeer/View/LoginView.swift index de4ae61..f4b51f6 100644 --- a/ios/PushDeer-iOS/PushDeer/View/LoginView.swift +++ b/ios/PushDeer-iOS/PushDeer/View/LoginView.swift @@ -10,6 +10,10 @@ import AuthenticationServices /// 登录界面 struct LoginView: View { + + @EnvironmentObject private var store: AppState + @State private var showLoading = false + var body: some View { VStack{ Spacer() @@ -17,40 +21,28 @@ struct LoginView: View { .resizable() .scaledToFit() Spacer() - AppleSignInButton( - onRequest: { request in - request.requestedScopes = [.fullName, .email] - }, - onCompletion: { result in - switch result { - case let .success(authorization): - if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { - // 用户唯一ID,在一个开发者账号下的APP获取到的是一样的 - print(appleIDCredential.user) // 000791.7a323f1326dd4674bc16d32fd6339875.1424 - // 注意:当第一次认证成功之后,将不会再返回email,fullName等信息,可以在设置->Apple ID->密码与安全性->使用您AppleID的App 中删除对应的APP。 - print(appleIDCredential.email as Any) // easychen@qq.com - print(appleIDCredential.fullName as Any) // givenName: lijie familyName: chen - // 「JWT」格式的token,用于验证信息合法性。其值用.分割成3段, 中间一段base64后会看到包含了 用户唯一标识 和 邮箱 等字段 - let idToken = String(data:appleIDCredential.identityToken!, encoding: .utf8) - print(idToken as Any) - - Task { - do { - let result = try await HttpRequest.login(idToken: idToken!) - - print(result) - } catch { - print(error) - } - } + if showLoading { + ProgressView() + .scaleEffect(1.5) + .frame(height: 64) + } else { + AppleSignInButton( + onRequest: { request in + request.requestedScopes = [.fullName, .email] + }, + onCompletion: { result in + do { + showLoading = true + store.token = try await store.appleIdLogin(result).token + // 获取成功去主页 + } catch { + showLoading = false } - case let .failure(error): - print(error) } - } - ) - .frame(maxWidth: 375, minHeight: 64, maxHeight: 64) - .padding() + ) + .frame(maxWidth: 375, minHeight: 64, maxHeight: 64) + .padding() + } Spacer() } .padding() diff --git a/ios/PushDeer-iOS/PushDeer/View/MainView.swift b/ios/PushDeer-iOS/PushDeer/View/MainView.swift index 6fe1d89..b78e45e 100644 --- a/ios/PushDeer-iOS/PushDeer/View/MainView.swift +++ b/ios/PushDeer-iOS/PushDeer/View/MainView.swift @@ -9,32 +9,38 @@ import SwiftUI /// APP 主界面 struct MainView: View { + @EnvironmentObject private var store: AppState + var body: some View { - TabView { + TabView.init(selection: $store.tabSelectedIndex) { DeviceListView() .tabItem { Label("设备",systemImage: "ipad.and.iphone") } + .tag(0) KeyListView() .tabItem{ Label("Key",systemImage: "key") } + .tag(1) MessageListView() .tabItem({Label("消息",systemImage: "message")}).onTapGesture { } + .tag(2) SettingsView() .tabItem{ Label("设置",systemImage: "gearshape") } + .tag(3) } } } struct MainView_Previews: PreviewProvider { static var previews: some View { - MainView() + MainView().environmentObject(AppState.shared) } } diff --git a/ios/PushDeer-iOS/PushDeer/View/MessageItemView.swift b/ios/PushDeer-iOS/PushDeer/View/MessageItemView.swift index 252e73c..cc1a977 100644 --- a/ios/PushDeer-iOS/PushDeer/View/MessageItemView.swift +++ b/ios/PushDeer-iOS/PushDeer/View/MessageItemView.swift @@ -6,8 +6,12 @@ // import SwiftUI +import MarkdownUI +import SDWebImageSwiftUI +import Photos struct MessageItemView: View { + let messageItem: MessageItem /// 删除按钮点击的回调 let deleteAction : () -> () @@ -23,7 +27,7 @@ struct MessageItemView: View { Text("key名字") .font(.system(size: 14)) .foregroundColor(Color(UIColor.darkGray)) - Text("· 5分钟前") + Text(messageItem.createdDateStr) .font(.system(size: 12)) .foregroundColor(Color(UIColor.darkGray)) HLine().stroke(Color(UIColor.lightGray)) @@ -31,33 +35,122 @@ struct MessageItemView: View { } DeletableView(contentView: { - CardView { - HStack{ - Text("纯文本的效果") - .font(.system(size: 14)) - .foregroundColor(Color(UIColor.darkGray)) - .padding() - Spacer(minLength: 0) - } - .contextMenu { - Button("复制") { - UIPasteboard.general.string = "someText" - } - } - } + MessageContentView(messageItem: messageItem) }, deleteAction: deleteAction) - .padding(EdgeInsets(top: 10, leading: 26, bottom: 0, trailing: 24)) + .padding(messageItem.type == "image" ? EdgeInsets.init() : EdgeInsets(top: 10, leading: 26, bottom: 0, trailing: 24)) } .padding(.top, 25) } } +struct MessageContentView: View { + let messageItem: MessageItem + @State private var image: PlatformImage? = nil + + var body: some View { + switch messageItem.type { + case "markdown": + CardView { + VStack(alignment: .leading, spacing: 5) { + Markdown(Document(messageItem.text)) + .markdownStyle( + DefaultMarkdownStyle( + font: .system(size: 14), + foregroundColor: UIColor.darkGray + ) + ) + if !messageItem.desp.isEmpty { + Markdown(Document(messageItem.desp)) + .markdownStyle( + DefaultMarkdownStyle( + font: .system(size: 14), + foregroundColor: UIColor.darkGray + ) + ) + } + } + .padding() + } + + case "image": + WebImage(url: URL(string: messageItem.text)) + .onSuccess { image, data, cacheType in + self.image = image + } + .resizable() + .scaledToFill() + .contextMenu { + Button { + UIPasteboard.general.image = image + HToast.showSuccess("已拷贝") + } label: { + Label("拷贝图片",systemImage: "doc.on.doc") + } + Button { + guard let image = image else { return } + PHPhotoLibrary.shared().performChanges { + PHAssetChangeRequest.creationRequestForAsset(from: image) + } completionHandler: { (isSuccess, error) in + DispatchQueue.main.async { + if isSuccess {// 成功 + print("Success") + HToast.showSuccess("保存成功") + } else { + print(error as Any) + HToast.showError("保存失败") + } + } + } + } label: { + Label("保存图片",systemImage: "square.and.arrow.down") + } + } + + default: + CardView { + VStack(alignment: .leading, spacing: 5) { + HStack{ + Text(messageItem.text) + .font(.system(size: 14)) + .foregroundColor(Color(UIColor.darkGray)) + Spacer(minLength: 0) + } + if !messageItem.desp.isEmpty { + Text(messageItem.desp) + .font(.system(size: 14)) + .foregroundColor(Color(UIColor.darkGray)) + } + } + .padding() + .contextMenu { + Button { + UIPasteboard.general.string = messageItem.text + messageItem.desp + HToast.showSuccess("已复制") + } label: { + Label("复制",systemImage: "doc.on.doc") + } + } + } + } + } +} + struct MessageItemView_Previews: PreviewProvider { static var previews: some View { VStack { - MessageItemView(){} - MessageItemView(){} + MessageItemView(messageItem: MessageItem(id: 1, uid: "1", text: "纯文本的效果", desp: "你好呀", type: "text", created_at: "2022-01-08T18:00:48.000000Z")){} + MessageItemView(messageItem: MessageItem(id: 1, uid: "1", text: "纯文本的效果纯文本的效果纯文本的效果纯文本的效果纯文本的效果纯文本的效果纯文本的效果", desp: "", type: "text", created_at: "2022-01-08T18:00:48.000000Z")){} + MessageItemView(messageItem: MessageItem(id: 1, uid: "1", text: "https://blog.wskfz.com/usr/uploads/2018/06/2498727457.png", desp: "", type: "image", created_at: "2022-01-08T18:00:48.000000Z")){} + MessageItemView(messageItem: MessageItem(id: 1, uid: "1", text: "https://blog.wskfz.com/usr/uploads/2018/06/2151130181.png", desp: "", type: "image", created_at: "2022-01-08T18:00:48.000000Z")){} + MessageItemView(messageItem: MessageItem(id: 1, uid: "1", text: "https://blog.wskfz.com/usr/uploads/2018/06/1718629805.png", desp: "", type: "image", created_at: "2022-01-08T18:00:48.000000Z")){} + MessageItemView(messageItem: MessageItem(id: 1, uid: "1", text: "*MarkDown*的**效果**", desp: "*MarkDown*的**效果**", type: "markdown", created_at: "2021-12-28T13:44:48.000000Z")){} + MessageItemView(messageItem: MessageItem(id: 1, uid: "1", text: """ +It's very easy to make some words **bold** and other words *italic* with Markdown. + +**Want to experiment with Markdown?** Play with the [reference CommonMark +implementation](https://spec.commonmark.org/dingus/). +""", desp: "", type: "markdown", created_at: "2021-12-28T13:44:48.000000Z")){} Spacer() } } diff --git a/ios/PushDeer-iOS/PushDeer/View/MessageListView.swift b/ios/PushDeer-iOS/PushDeer/View/MessageListView.swift index 9680ee9..5cda46b 100644 --- a/ios/PushDeer-iOS/PushDeer/View/MessageListView.swift +++ b/ios/PushDeer-iOS/PushDeer/View/MessageListView.swift @@ -9,21 +9,27 @@ import SwiftUI /// 消息界面 struct MessageListView: View { - - @State private var messages = Array(0..<10) - @State private var isShowTest = true + @EnvironmentObject private var store: AppState var body: some View { BaseNavigationView(title: "消息") { ScrollView { LazyVStack(alignment: .leading) { - if isShowTest { + if store.isShowTestPush { TestPushView() } - ForEach(messages, id: \.self) { msg in - MessageItemView { - messages.removeAll { _msg in - _msg == msg + ForEach(store.messages) { messageItem in + MessageItemView(messageItem: messageItem) { + store.messages.removeAll { _messageItem in + _messageItem.id == messageItem.id + } + HToast.showSuccess("已删除") + Task { + do { + _ = try await HttpRequest.rmMessage(id: messageItem.id) + } catch { + + } } } } @@ -32,17 +38,23 @@ struct MessageListView: View { } .navigationBarItems(trailing: Button(action: { withAnimation(.easeOut) { - isShowTest = !isShowTest + store.isShowTestPush = !store.isShowTestPush } }, label: { - Image(systemName: isShowTest ? "chevron.up" : "chevron.down") + Image(systemName: store.isShowTestPush ? "chevron.up" : "chevron.down") .foregroundColor(Color(UIColor.lightGray)) })) } + .onAppear { + Task { + store.messages = try await HttpRequest.getMessages().messages + } + } } } struct TestPushView: View { + @EnvironmentObject private var store: AppState @State private var testText = "" var body: some View { TextEditor(text: $testText) @@ -52,6 +64,26 @@ struct TestPushView: View { Button("推送测试") { print("点击推送测试") + if testText.isEmpty { + HToast.showError("推送失败, 请先输入推送内容") + return + } + Task { + if store.keys.isEmpty { + store.keys = try await HttpRequest.getKeys().keys + } + if let keyItem = store.keys.first { + _ = try await HttpRequest.push(pushkey: keyItem.key, text: testText, desp: "", type: "") + testText = "" + HToast.showSuccess("推送成功") + let messages = try await HttpRequest.getMessages().messages + withAnimation(.easeOut) { + store.messages = messages + } + } else { + HToast.showError("推送失败, 请先添加一个Key") + } + } } .font(.system(size: 20)) .frame(width: 104, height: 42) diff --git a/ios/PushDeer-iOS/PushDeer/View/SettingsItemView.swift b/ios/PushDeer-iOS/PushDeer/View/SettingsItemView.swift index b40ffd8..8fba721 100644 --- a/ios/PushDeer-iOS/PushDeer/View/SettingsItemView.swift +++ b/ios/PushDeer-iOS/PushDeer/View/SettingsItemView.swift @@ -18,8 +18,8 @@ struct SettingsItemView: View { Text(title) .font(.system(size: 18)) .foregroundColor(Color(UIColor.darkGray)) - .padding() - Spacer() + .padding(.leading, 16) + Spacer(minLength: 0) Button(button) { print("点击\(button)") action() diff --git a/ios/PushDeer-iOS/PushDeer/View/SettingsView.swift b/ios/PushDeer-iOS/PushDeer/View/SettingsView.swift index 8147033..d717acd 100644 --- a/ios/PushDeer-iOS/PushDeer/View/SettingsView.swift +++ b/ios/PushDeer-iOS/PushDeer/View/SettingsView.swift @@ -6,32 +6,49 @@ // import SwiftUI +//import StoreKit /// 设置界面 struct SettingsView: View { + @EnvironmentObject private var store: AppState + var body: some View { BaseNavigationView(title: "设置") { VStack { - SettingsItemView(title: "登录为 Hext", button: "退出") { + SettingsItemView(title: "登录为 \(store.userInfo?.name ?? "--")", button: "退出") { + store.token = "" } .padding(EdgeInsets(top: 18, leading: 20, bottom: 0, trailing: 20)) SettingsItemView(title: "自定义服务器", button: "扫码") { } + .disabled(true) .padding(EdgeInsets(top: 18, leading: 20, bottom: 0, trailing: 20)) SettingsItemView(title: "喜欢PushDeer?", button: "评分") { + let urlStr = "itms-apps://itunes.apple.com/app/id\(1596771139)?action=write-review" + UIApplication.shared.open(URL(string: urlStr)!, options: [:], completionHandler: nil) + // 直接弹出系统评分控件, 不过一年最多3次, 用户还可以在系统设置里面关 +// SKStoreReviewController.requestReview() } .padding(EdgeInsets(top: 18, leading: 20, bottom: 0, trailing: 20)) Spacer() } } + .onAppear { + if store.userInfo != nil { + return + } + Task { + store.userInfo = try await HttpRequest.getUserInfo() + } + } } } struct SettingsView_Previews: PreviewProvider { static var previews: some View { - SettingsView() + SettingsView().environmentObject(AppState.shared) } } diff --git a/ios/PushDeer-iOS/PushDeerClip/ContentView.swift b/ios/PushDeer-iOS/PushDeerClip/ContentView.swift deleted file mode 100644 index bd475d1..0000000 --- a/ios/PushDeer-iOS/PushDeerClip/ContentView.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ContentView.swift -// PushDeerClip -// -// Created by HEXT on 2021/12/30. -// - -import SwiftUI - -struct ContentView: View { - var body: some View { - MainView() - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/ios/PushDeer-iOS/PushDeerClip/Info.plist b/ios/PushDeer-iOS/PushDeerClip/Info.plist index 301b5a0..8dd2867 100644 --- a/ios/PushDeer-iOS/PushDeerClip/Info.plist +++ b/ios/PushDeer-iOS/PushDeerClip/Info.plist @@ -2,11 +2,6 @@ - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - NSAppClip NSAppClipRequestEphemeralUserNotification @@ -14,5 +9,20 @@ NSAppClipRequestLocationConfirmation + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSBonjourServices + + _adhp._tcp + + NSLocalNetworkUsageDescription + APP需要访问本地网络以供调试(仅在开发时) + CFBundleAllowMixedLocalizations + + NSPhotoLibraryAddUsageDescription + APP为您提供保存图片到相册的功能