105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | ### 六、特别感谢
118 |
119 | - [Alamofire/Alamofire](https://github.com/Alamofire/Alamofire)
120 | - [Kitura/Swift-SMTP](https://github.com/Kitura/Swift-SMTP)
121 | - [SnapKit/SnapKit](https://github.com/SnapKit/SnapKit)
122 | - [sparkle-project/Sparkle](https://github.com/sparkle-project/Sparkle)
123 | - [tid-kijyun/Kanna](https://github.com/tid-kijyun/Kanna)
124 | - [drmohundro/SWXMLHash](https://github.com/drmohundro/SWXMLHash)
125 | - [jdg/MBProgressHUD](https://github.com/jdg/MBProgressHUD)
126 | - [joshuajylin/MBProgressHUD-macOS](https://github.com/joshuajylin/MBProgressHUD-macOS)
127 | - [Yueoaix/SymbolicatorX](https://github.com/Yueoaix/SymbolicatorX)
128 | - [fpotter/ExpandingDatePicker](https://github.com/fpotter/ExpandingDatePicker)
129 | - [kishikawakatsumi/KeychainAccess](https://github.com/kishikawakatsumi/KeychainAccess)
130 | - [AvdLee/appstoreconnect-swift-sdk](https://github.com/AvdLee/appstoreconnect-swift-sdk)
131 |
132 |
133 |
--------------------------------------------------------------------------------
/AppleParty/Shared/Info/InfoCenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoCenter.swift
3 | // AppleParty
4 | //
5 | // Created by HTC on 2022/3/17.
6 | // Copyright © 2022 37 Mobile Games. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | let InfoCenterKey_Session_Key = "InfoCenterKey_Session_Key"
12 | let InfoCenterKey_TrusDevice_Key = "InfoCenterKey_TrusDevice_Key"
13 |
14 | /// AppStoreConnect 密钥模型
15 | struct AppStoreConnectKey: Codable {
16 | var aliasName: String
17 | var issuerID: String
18 | var privateKeyID: String
19 | var privateKey: String
20 | var isused: Bool
21 |
22 | mutating func model(_ asck: AppStoreConnectKey, _ isused: Bool) -> AppStoreConnectKey {
23 | AppStoreConnectKey(aliasName: asck.aliasName, issuerID: asck.issuerID, privateKeyID: asck.privateKeyID, privateKey: asck.privateKey, isused: isused)
24 | }
25 | }
26 |
27 |
28 | /// 信息模型
29 | struct AppleInfoSession: Codable {
30 | var scnt: String
31 | var sessionId: String
32 | var cookies: Data
33 | var ascKeys: [AppStoreConnectKey]
34 | }
35 |
36 |
37 | struct InfoCenter {
38 | static var shared = InfoCenter()
39 |
40 | var session: AppleInfoSession {
41 | set {
42 | let encoder = JSONEncoder()
43 | if let data = try? encoder.encode(newValue) {
44 | #if DEBUG
45 | UserDefaults.standard.set(data, forKey: InfoCenterKey_Session_Key)
46 | #else
47 | try? APUtil.keychain.set(data, key: InfoCenterKey_Session_Key)
48 | #endif
49 | }
50 | }
51 | get {
52 | var data: Data?
53 | #if DEBUG
54 | data = UserDefaults.standard.data(forKey: InfoCenterKey_Session_Key)
55 | #else
56 | data = try? APUtil.keychain.getData(InfoCenterKey_Session_Key)
57 | #endif
58 | if let data = data, let model = try? JSONDecoder().decode(AppleInfoSession.self, from: data) {
59 | return model
60 | } else {
61 | return AppleInfoSession(scnt: "", sessionId: "", cookies: Data(), ascKeys: [])
62 | }
63 | }
64 | }
65 |
66 | var scnt: String {
67 | get {
68 | session.scnt
69 | }
70 | set {
71 | session = AppleInfoSession(scnt: newValue, sessionId: session.sessionId, cookies: session.cookies, ascKeys: session.ascKeys)
72 | }
73 | }
74 |
75 | var sessionId: String {
76 | get {
77 | session.sessionId
78 | }
79 | set {
80 | session = AppleInfoSession(scnt: session.scnt, sessionId: newValue, cookies: session.cookies, ascKeys: session.ascKeys)
81 | }
82 | }
83 |
84 | var cookies: [HTTPCookie] {
85 | get {
86 | let data = session.cookies
87 | // NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, HTTPCookie.self], from: data)
88 | // adopt NSSecureCoding. Class 'NSHTTPCookie' does not adopt it
89 | let cookies = try? NSKeyedUnarchiver.unarchiveObject(with: data)
90 | return cookies as? [HTTPCookie] ?? []
91 | }
92 | set {
93 | let cookieData = (try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: false)) ?? Data()
94 | session = AppleInfoSession(scnt: session.scnt, sessionId: session.sessionId, cookies: cookieData, ascKeys: session.ascKeys)
95 | }
96 | }
97 |
98 | var ascKeys: [AppStoreConnectKey] {
99 | get {
100 | session.ascKeys
101 | }
102 | set {
103 | session = AppleInfoSession(scnt: session.scnt, sessionId: session.sessionId, cookies: session.cookies, ascKeys: newValue)
104 | }
105 | }
106 |
107 | var currentASCKey: AppStoreConnectKey? {
108 | get {
109 | let accounts = self.ascKeys
110 | let models = accounts.filter({ $0.isused == true })
111 | return models.first
112 | }
113 | }
114 |
115 | var trusDevice: Bool {
116 | get {
117 | if let trus = APUtil.defaults.bool(forKey: InfoCenterKey_TrusDevice_Key) {
118 | return trus
119 | }
120 | return true //默认为信任设备
121 | }
122 | set {
123 | APUtil.defaults.set(newValue, forKey: InfoCenterKey_TrusDevice_Key)
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/AppleParty/Shared/UI/APDebugVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APDebugVC.swift
3 | // AppleParty
4 | //
5 | // Created by HTC on 2022/3/21.
6 | // Copyright © 2022 37 Mobile Games. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class APDebugVC: NSViewController {
12 |
13 | @IBOutlet var debugTextView: NSTextView!
14 | @IBOutlet weak var refreshBtn: NSButton!
15 | @IBOutlet weak var shareBtn: NSButton!
16 |
17 | var debugLog: String = "" {
18 | didSet {
19 | uploadData()
20 | }
21 | }
22 |
23 | var fileURL: URL? {
24 | didSet {
25 | refreshBtn.isHidden = false
26 | reloadFileLogs()
27 | }
28 | }
29 |
30 | // Menu
31 | private lazy var editMenu: NSMenu = {
32 | let menu = NSMenu()
33 | let editMenuItems = [
34 | NSMenuItem(title: "邮件发送", action: #selector(emailShare), keyEquivalent: ""),
35 | NSMenuItem(title: "隔空投送", action: #selector(airDropShare), keyEquivalent: ""),
36 | NSMenuItem(title: "其它方式", action: #selector(otherShare), keyEquivalent: ""),
37 | ]
38 | for editMenuItem in editMenuItems {
39 | menu.addItem(editMenuItem)
40 | }
41 | return menu
42 | }()
43 |
44 | override func viewDidLoad() {
45 | super.viewDidLoad()
46 | }
47 |
48 | @IBAction func clickedRefreshBtn(_ sender: Any) {
49 | reloadFileLogs()
50 | }
51 |
52 | @IBAction func clickedShareBtn(_ sender: NSButton) {
53 | let p = NSPoint(x: sender.frame.width, y: 0) //按钮右边
54 | self.editMenu.popUp(positioning: nil, at: p, in: sender)
55 | }
56 |
57 | }
58 |
59 |
60 | extension APDebugVC {
61 |
62 | func uploadData() {
63 | DispatchQueue.main.async {
64 | self.debugTextView.string = self.debugLog
65 | self.debugTextView.scrollRangeToVisible(NSMakeRange(self.debugTextView.string.count, 0))
66 | }
67 | }
68 |
69 |
70 | func reloadFileLogs() {
71 | guard let file = fileURL, let logs = try? String(contentsOf: file, encoding: .utf8) else {
72 | debugTextView.string = "读取日志失败!\(String(describing: fileURL?.path))"
73 | return
74 | }
75 | debugLog = logs
76 | }
77 |
78 | func getTextFileURL(text: String) -> URL? {
79 |
80 | guard let data = text.data(using: .utf8) else { return nil }
81 |
82 | let dateFormatter = DateFormatter()
83 | dateFormatter.dateFormat = "yyyyMMdd_HHmm"
84 | let date = dateFormatter.string(from: Date())
85 | let filename = "AppleParty-Logs_\(date).txt"
86 |
87 | let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
88 | .appendingPathComponent(filename)
89 |
90 | do {
91 | try data.write(to: tempURL, options: .atomic)
92 |
93 | return tempURL
94 | } catch {
95 | assertionFailure("Failed to write temporary URL for pasteboard: \(String(describing: error))")
96 | return nil
97 | }
98 | }
99 | }
100 |
101 |
102 | extension APDebugVC {
103 |
104 | @objc func emailShare() {
105 | let text = debugTextView.string
106 | let mainStoryBoard = NSStoryboard(name: "EmailTool", bundle: nil)
107 | let windowController = mainStoryBoard.instantiateController(withIdentifier: "EmailTool") as! NSWindowController
108 | let controller = windowController.contentViewController as! EmailToolVC
109 | controller.emailTitle = "苹果派-错误日志"
110 | controller.emailContent = text
111 | controller.attachmentFileUrl = getTextFileURL(text: text)
112 | windowController.showWindow(self)
113 | }
114 |
115 | @objc func airDropShare() {
116 | let text = debugTextView.string
117 | guard let url = getTextFileURL(text: text) else {
118 | otherShare()
119 | return
120 | }
121 |
122 | let service = NSSharingService(named: .sendViaAirDrop)!
123 | let items: [NSURL] = [url as NSURL]
124 | if service.canPerform(withItems: items) {
125 | service.perform(withItems: items)
126 | } else {
127 | NSAlert.show("Cannot perform AirDrop!")
128 | }
129 | }
130 |
131 | @objc func otherShare() {
132 | var item: Any = debugTextView.string
133 | if let text = item as? String, let url = getTextFileURL(text: text) {
134 | item = url
135 | }
136 | let picker = NSSharingServicePicker(items: [item])
137 | picker.show(relativeTo: .zero, of: shareBtn, preferredEdge: .maxX)
138 | }
139 |
140 | }
141 |
--------------------------------------------------------------------------------
/AppleParty/LoginView/AppleWebLogin/AppleWebLoginCore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppleWebLoginCore.swift
3 | // AppleWebLogin
4 | //
5 | // Created by 秋星桥 on 2024/10/23.
6 | // ref: https://github.com/Lakr233/AppleWebLogin
7 |
8 | import Combine
9 | @preconcurrency import WebKit
10 |
11 | //private let loginURL = URL(string: "https://account.apple.com/sign-in")!
12 | private let loginURL = URL(string: "https://appstoreconnect.apple.com/login")!
13 |
14 | public class AppleWebLoginCore: NSObject, WKUIDelegate, WKNavigationDelegate {
15 | var webView: WKWebView {
16 | associatedWebView
17 | }
18 |
19 | private let associatedWebView: WKWebView
20 | private var dataPopulationTimer: Timer? = nil
21 | private var firstLoadComplete = false
22 |
23 | public private(set) var onFirstLoadComplete: (() -> Void)?
24 | public var onCredentialPopulation: ((String, [HTTPCookie]) -> Void)?
25 |
26 | override public init() {
27 | let contentController = WKUserContentController()
28 | let configuration = WKWebViewConfiguration()
29 | configuration.defaultWebpagePreferences.allowsContentJavaScript = true
30 | configuration.userContentController = contentController
31 | configuration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
32 | configuration.websiteDataStore = .nonPersistent()
33 |
34 | associatedWebView = .init(
35 | frame: CGRect(x: 0, y: 0, width: 1920, height: 1080),
36 | configuration: configuration
37 | )
38 | associatedWebView.isHidden = true
39 |
40 | super.init()
41 |
42 | associatedWebView.uiDelegate = self
43 | associatedWebView.navigationDelegate = self
44 |
45 | associatedWebView.load(.init(url: loginURL))
46 |
47 | #if DEBUG
48 | if associatedWebView.responds(to: Selector(("setInspectable:"))) {
49 | associatedWebView.perform(Selector(("setInspectable:")), with: true)
50 | }
51 | #endif
52 |
53 | let dataPopulationTimer = Timer(timeInterval: 1, repeats: true) { [weak self] _ in
54 | guard let self else { return }
55 | removeUnwantedElements()
56 | populateData()
57 | }
58 | RunLoop.main.add(dataPopulationTimer, forMode: .common)
59 | self.dataPopulationTimer = dataPopulationTimer
60 | }
61 |
62 | deinit {
63 | dataPopulationTimer?.invalidate()
64 | onCredentialPopulation = nil
65 | }
66 |
67 | public func webView(_: WKWebView, didFinish _: WKNavigation!) {
68 | guard !firstLoadComplete else { return }
69 | defer { firstLoadComplete = true }
70 | associatedWebView.isHidden = false
71 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
72 | self.onFirstLoadComplete?()
73 | self.onFirstLoadComplete = nil
74 | }
75 | }
76 |
77 | // public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
78 | // let request = navigationAction.request
79 | // if let headers = request.allHTTPHeaderFields {
80 | // print(request.url?.absoluteString)
81 | // print("headers: \(headers)")
82 | // if let scntValue = headers["scnt"] {
83 | // print("scnt value: \(scntValue)")
84 | // }
85 | // }
86 | // decisionHandler(.allow, preferences)
87 | // }
88 |
89 | public func installFirstLoadCompleteTrap(_ block: @escaping () -> Void) {
90 | onFirstLoadComplete = block
91 | }
92 |
93 | public func installCredentialPopulationTrap(_ block: @escaping (String, [HTTPCookie]) -> Void) {
94 | onCredentialPopulation = block
95 | }
96 |
97 | private func removeUnwantedElements() {
98 | let removeElements = """
99 | Element.prototype.remove = function() {
100 | this.parentElement.removeChild(this);
101 | }
102 | NodeList.prototype.remove = HTMLCollection.prototype.remove = function() {
103 | for(var i = this.length - 1; i >= 0; i--) {
104 | if(this[i] && this[i].parentElement) {
105 | this[i].parentElement.removeChild(this[i]);
106 | }
107 | }
108 | }
109 | document.getElementById("header").remove();
110 | document.getElementsByClassName('landing__animation').remove();
111 | """
112 | associatedWebView.evaluateJavaScript(removeElements) { _, _ in
113 | }
114 | }
115 |
116 | private func populateData() {
117 | guard let onCredentialPopulation else { return }
118 | associatedWebView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
119 | //print(cookies)
120 | for cookie in cookies where cookie.name == "myacinfo" {
121 | let value = cookie.value
122 | onCredentialPopulation(value, cookies)
123 | self.onCredentialPopulation = nil
124 | }
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/AppleParty/LoginView/APWebLoginVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APWebLoginVC.swift
3 | // AppleParty
4 | //
5 | // Created by HTC on 2024/10/29.
6 | // Copyright © 2024 37 Mobile Games. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class APWebLoginVC: NSViewController {
12 |
13 | public var cancelHandle: (() -> Void)?
14 | public var successHandle: (() -> Void)?
15 | private var webCore: AppleWebLoginCore? = nil
16 |
17 | @IBOutlet weak var loginBtn: NSButton!
18 | @IBOutlet weak var cancelBtn: NSButton!
19 | @IBOutlet weak var indicatorView: NSProgressIndicator!
20 | @IBOutlet weak var tipsWarningView: NSTextField!
21 |
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 | // Do view setup here.
25 | }
26 |
27 | @IBAction func clickedCancelBtn(_ sender: NSButton) {
28 | closeView()
29 | cancelHandle?()
30 | }
31 |
32 | @IBAction func clickedLoginBtn(_ sender: NSButton) {
33 | validateSession()
34 | }
35 |
36 | // 判断是登陆态是否过期
37 | func validateSession() {
38 | viewEnabled(false)
39 | APClient.signInSession.request { [weak self] result, response, error in
40 | self?.viewEnabled(true)
41 | let code = response?.statusCode
42 | switch code {
43 | case 200, 201:
44 | UserCenter.shared.isAuthorized = true
45 | self?.successHandle?()
46 | self?.closeView()
47 | default:
48 | let errors = dictionaryArray(result["serviceErrors"])
49 | let msg = string(from: errors.first?["message"])
50 | self?.showTips(msg.isEmpty ? error.debugDescription : msg)
51 | // 隐藏按钮透视显示
52 | self?.cancelBtn.isEnabled = false
53 | self?.loginBtn.isEnabled = false
54 | self?.loginWithWeb()
55 | }
56 | }
57 | }
58 |
59 | func loginWithWeb() {
60 | let appleWebLoginCore = AppleWebLoginCore()
61 | // 将 webView 添加到视图层次结构中
62 | self.view.addSubview(appleWebLoginCore.webView)
63 |
64 | let closeButton = NSButton(title: "取消", target: self, action: #selector(closeButtonClicked))
65 | closeButton.attributedTitle = NSAttributedString(string: "取消", attributes: [NSAttributedString.Key.foregroundColor: NSColor.gray])
66 | closeButton.keyEquivalent = "\u{1B}" // `esc` 快捷键
67 | appleWebLoginCore.webView.addSubview(closeButton)
68 |
69 | // 设置 webView 的约束以适应视图
70 | appleWebLoginCore.webView.translatesAutoresizingMaskIntoConstraints = false
71 | closeButton.translatesAutoresizingMaskIntoConstraints = false
72 | NSLayoutConstraint.activate([
73 | appleWebLoginCore.webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
74 | appleWebLoginCore.webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
75 | appleWebLoginCore.webView.topAnchor.constraint(equalTo: self.view.topAnchor),
76 | appleWebLoginCore.webView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
77 |
78 | closeButton.trailingAnchor.constraint(equalTo: appleWebLoginCore.webView.trailingAnchor, constant: -10),
79 | closeButton.topAnchor.constraint(equalTo: appleWebLoginCore.webView.topAnchor, constant: 10)
80 | ])
81 |
82 | // 关闭按钮
83 |
84 |
85 | appleWebLoginCore.installFirstLoadCompleteTrap {
86 | // 处理首次加载完成的逻辑
87 | print("First load complete")
88 | }
89 |
90 | appleWebLoginCore.installCredentialPopulationTrap { token, cookies in
91 | // 处理凭据填充的逻辑
92 | print("Received cookies: \(cookies)")
93 | print("Received token: \(token)")
94 |
95 | if let cks = APClientSession.shared.config.httpCookieStorage?.cookies {
96 | for ck in cks {
97 | APClientSession.shared.config.httpCookieStorage?.deleteCookie(ck)
98 | }
99 | }
100 | for cookie in cookies {
101 | APClientSession.shared.config.httpCookieStorage?.setCookie(cookie)
102 | }
103 | // APClientSession.shared.config.headers.update(name: "Cookie", value: "myacinfo=\(token);")
104 | self.validateSession()
105 | }
106 | self.webCore = appleWebLoginCore
107 | }
108 |
109 | @objc func closeButtonClicked() {
110 | // 处理关闭按钮的点击事件
111 | print("关闭按钮被点击")
112 | closeView()
113 | }
114 |
115 | func viewEnabled(_ isEnabled: Bool) {
116 | showTips("")
117 | loginBtn.isEnabled = isEnabled
118 | isEnabled ? indicatorView.stopAnimation(nil) : indicatorView.startAnimation(nil)
119 | }
120 |
121 | func showTips(_ text: String) {
122 | if text.isEmpty {
123 | tipsWarningView.isHidden = true
124 | tipsWarningView.stringValue = ""
125 | } else {
126 | tipsWarningView.stringValue = text
127 | tipsWarningView.isHidden = false
128 | }
129 | }
130 |
131 | func closeView() {
132 | guard let window = view.window, let parent = window.sheetParent
133 | else { return }
134 | parent.endSheet(window)
135 | }
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/AppleParty/RootView/APRootWC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APRootWC.swift
3 | // AppleParty
4 | //
5 | // Created by HTC on 2022/3/11.
6 | // Copyright © 2022 37 Mobile Games. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class APRootWC: NSWindowController {
12 |
13 | override func windowDidLoad() {
14 | super.windowDidLoad()
15 | setupUI()
16 | }
17 |
18 | func setupUI() {
19 | self.window?.title = "AppleParty"
20 | if #available(macOS 11.0, *) {
21 | self.window?.subtitle = "37 Mobile Games"
22 | }
23 | }
24 |
25 | @IBAction func clickedAccountItem(_ sender: NSToolbarItem?) {
26 | // 登陆时,显示切换账号
27 | if UserCenter.shared.isAuthorized {
28 | if let providers = UserCenter.shared.accountProviders["availableProviders"] as? [[String: Any]] {
29 | var accountList: [Provider] = []
30 | providers.forEach { provider in
31 | accountList.append(Provider(name: provider["name"] as! String, providerId: String(provider["providerId"] as! Int), publicProviderId: provider["publicProviderId"] as! String))
32 | }
33 |
34 | accountList.append(Provider(name: "账号登出", providerId: "", publicProviderId: ""))
35 |
36 | let listVC = APSwichAccountPopover()
37 | listVC.accounts = accountList
38 | listVC.selectHandle = { [weak self] row in
39 | guard row >= 0, row < accountList.count else {
40 | return
41 | }
42 | let publicProviderId = accountList[row].publicProviderId
43 | self?.switchAccount(publicProviderId)
44 | }
45 | let pannel = NSPanel(contentViewController: listVC)
46 | pannel.setFrame(NSRect(origin: .zero, size: NSSize(width: 300, height: 350)), display: true)
47 | window?.beginSheet(pannel, completionHandler: nil)
48 | } else {
49 | APHUD.hide(message:"获取登陆账号信息异常~")
50 | }
51 |
52 | } else {
53 | let vc = APWebLoginVC()
54 | vc.successHandle = { [weak self] in
55 | self?.fetchAccountTeamInfo()
56 | self?.window?.title = UserCenter.shared.developerName
57 | if #available(macOS 11.0, *) {
58 | self?.window?.subtitle = UserCenter.shared.accountEmail
59 | }
60 | }
61 | let pannel = NSPanel(contentViewController: vc)
62 | pannel.setFrame(NSRect(origin: .zero, size: NSSize(width: 550, height: 450)), display: true)
63 | window?.beginSheet(pannel, completionHandler: nil)
64 | }
65 | }
66 |
67 | @IBAction func clickedSettingsItem(_ sender: Any) {
68 | let vc = APSettingVC()
69 | let window = NSWindow(contentViewController: vc)
70 | let wc = NSWindowController(window: window)
71 | wc.showWindow(self)
72 | vc.isLoginViewShow = !UserCenter.shared.isAuthorized
73 | }
74 |
75 | @IBAction func clickedGithubItem(_ sender: Any) {
76 | let url = URL(string: kApplePartyGitHub)
77 | NSWorkspace.shared.open(url!)
78 | }
79 |
80 | @IBAction func clicedFeedbackItem(_ sender: Any) {
81 | let url = URL(string: kApplePartyNewIssues)
82 | NSWorkspace.shared.open(url!)
83 | }
84 |
85 | @IBAction func cliced37MobileGamesItem(_ sender: Any) {
86 | let url = URL(string: k37MobileGamesSite)
87 | NSWorkspace.shared.open(url!)
88 | }
89 |
90 |
91 | @IBAction func cliced37iOSTeamItem(_ sender: Any) {
92 | let url = URL(string: k37iOSTeamJueJinSite)
93 | NSWorkspace.shared.open(url!)
94 | }
95 | }
96 |
97 |
98 | extension APRootWC {
99 |
100 | func switchAccount(_ publicProviderId: String) {
101 | guard publicProviderId.count > 0 else {
102 | UserCenter.shared.isAutoLogin = false
103 | UserCenter.shared.isAuthorized = false
104 | // 清掉缓存
105 | //HTTPCookieStorage.shared.cookies?.forEach(HTTPCookieStorage.shared.deleteCookie)
106 | InfoCenter.shared.cookies = []
107 | setupUI()
108 | return
109 | }
110 |
111 | APClient.switchProvider(publicProviderId: publicProviderId).request(showLoading: true) { [weak self] result, response, error in
112 | guard let err = error else {
113 | self?.validateSession()
114 | return
115 | }
116 | APHUD.hide(message: err.localizedDescription)
117 | }
118 | }
119 |
120 | func validateSession() {
121 | APClient.signInSession.request(showLoading: true) { [weak self] result, response, error in
122 | guard let err = error else {
123 | UserCenter.shared.isAuthorized = true
124 | self?.window?.title = UserCenter.shared.developerName
125 | if #available(macOS 11.0, *) {
126 | self?.window?.subtitle = UserCenter.shared.accountEmail
127 | }
128 | self?.fetchAccountTeamInfo()
129 | return
130 | }
131 | APHUD.hide(message: err.localizedDescription)
132 | }
133 | }
134 |
135 | func fetchAccountTeamInfo() {
136 | // 获取开发者 Team id 信息
137 | APClient.ascProvider.request(completionHandler: nil)
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/AppleParty/AppListView/InAppPurchseView/APInAppPurchseCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APInAppPurchseCell.swift
3 | // AppleParty
4 | //
5 | // Created by HTC on 2022/3/28.
6 | // Copyright © 2022 37 Mobile Games. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class APInAppPurchseCell: NSTableCellView {
12 |
13 |
14 | }
15 |
16 | class ImageViewCell: NSTableCellView {
17 |
18 | @IBOutlet weak var imgSel: NSImageView!
19 |
20 | override func awakeFromNib() {
21 | super.awakeFromNib()
22 | }
23 | }
24 |
25 | class UploadCell: NSTableCellView {
26 |
27 | var row: Int = 0
28 |
29 | @IBOutlet weak var imgSel: NSImageView!
30 | @IBOutlet weak var dragView: DragView!
31 | @IBOutlet weak var dragBox: NSView!
32 |
33 | typealias CallBackFunc = (_ path: String, _ row: Int) -> Void
34 | var callBackFunc: CallBackFunc?
35 |
36 | override func awakeFromNib() {
37 | super.awakeFromNib()
38 | dragView.delegate = self
39 | }
40 | }
41 |
42 | extension UploadCell: DragViewDelegate {
43 | func dragView(_ path: String?) {
44 | if let path = path {
45 | debugPrint(path)
46 | imgSel.image = NSImage(contentsOfFile: path)
47 | if let callBackFunc = callBackFunc {
48 | callBackFunc(path, row)
49 | }
50 | } else {
51 | imgSel.image = nil
52 | }
53 | }
54 | }
55 |
56 |
57 | enum ColumnIdetifier: String {
58 | case id
59 | case productID
60 | case productName
61 | case priceLevel
62 | case appleid
63 | case price
64 | case type
65 | case state
66 |
67 | // list
68 | case productPds
69 | case level
70 | case status
71 | case screenshot
72 | case language
73 | case upload
74 | case picname
75 |
76 | var columnValue: NSUserInterfaceItemIdentifier {
77 | return NSUserInterfaceItemIdentifier(rawValue: self.rawValue+"Column")
78 | }
79 | var cellValue: NSUserInterfaceItemIdentifier {
80 | return NSUserInterfaceItemIdentifier(rawValue: self.rawValue+"Cell")
81 | }
82 | }
83 |
84 | extension NSUserInterfaceItemIdentifier {
85 | func stringValue() -> String {
86 | switch self {
87 | case ColumnIdetifier.id.columnValue:
88 | return ColumnIdetifier.id.rawValue
89 | case ColumnIdetifier.productID.columnValue:
90 | return ColumnIdetifier.productID.rawValue
91 | case ColumnIdetifier.productName.columnValue:
92 | return ColumnIdetifier.productName.rawValue
93 | case ColumnIdetifier.price.columnValue:
94 | return ColumnIdetifier.price.rawValue
95 | case ColumnIdetifier.type.columnValue:
96 | return ColumnIdetifier.type.rawValue
97 | case ColumnIdetifier.state.columnValue:
98 | return ColumnIdetifier.state.rawValue
99 | case ColumnIdetifier.productPds.columnValue:
100 | return ColumnIdetifier.productPds.rawValue
101 | case ColumnIdetifier.level.columnValue:
102 | return ColumnIdetifier.level.rawValue
103 | case ColumnIdetifier.status.columnValue:
104 | return ColumnIdetifier.status.rawValue
105 | case ColumnIdetifier.appleid.columnValue:
106 | return ColumnIdetifier.appleid.rawValue
107 | case ColumnIdetifier.priceLevel.columnValue:
108 | return ColumnIdetifier.priceLevel.rawValue
109 | case ColumnIdetifier.screenshot.columnValue:
110 | return ColumnIdetifier.screenshot.rawValue
111 | case ColumnIdetifier.picname.columnValue:
112 | return ColumnIdetifier.picname.rawValue
113 | case ColumnIdetifier.upload.columnValue:
114 | return ColumnIdetifier.upload.rawValue
115 | case ColumnIdetifier.language.columnValue:
116 | return ColumnIdetifier.language.rawValue
117 | default:
118 | return "none"
119 | }
120 | }
121 |
122 | func enumValue() -> NSUserInterfaceItemIdentifier {
123 | switch self {
124 | case ColumnIdetifier.id.columnValue:
125 | return ColumnIdetifier.id.cellValue
126 | case ColumnIdetifier.productID.columnValue:
127 | return ColumnIdetifier.productID.cellValue
128 | case ColumnIdetifier.productName.columnValue:
129 | return ColumnIdetifier.productName.cellValue
130 | case ColumnIdetifier.price.columnValue:
131 | return ColumnIdetifier.price.cellValue
132 | case ColumnIdetifier.type.columnValue:
133 | return ColumnIdetifier.type.cellValue
134 | case ColumnIdetifier.state.columnValue:
135 | return ColumnIdetifier.state.cellValue
136 | case ColumnIdetifier.productPds.columnValue:
137 | return ColumnIdetifier.productPds.cellValue
138 | case ColumnIdetifier.level.columnValue:
139 | return ColumnIdetifier.level.cellValue
140 | case ColumnIdetifier.status.columnValue:
141 | return ColumnIdetifier.status.cellValue
142 | case ColumnIdetifier.appleid.columnValue:
143 | return ColumnIdetifier.appleid.cellValue
144 | case ColumnIdetifier.priceLevel.columnValue:
145 | return ColumnIdetifier.priceLevel.cellValue
146 | case ColumnIdetifier.screenshot.columnValue:
147 | return ColumnIdetifier.screenshot.cellValue
148 | case ColumnIdetifier.picname.columnValue:
149 | return ColumnIdetifier.picname.cellValue
150 | case ColumnIdetifier.upload.columnValue:
151 | return ColumnIdetifier.upload.cellValue
152 | case ColumnIdetifier.language.columnValue:
153 | return ColumnIdetifier.language.cellValue
154 | default:
155 | return NSUserInterfaceItemIdentifier(rawValue: "none")
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/AppleParty/IPAUpload/APIPAUploadVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIPAUploadVC.swift
3 | // AppleParty
4 | //
5 | // Created by HTC on 2022/5/12.
6 | // Copyright © 2022 37 Mobile Games. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class APIPAUploadVC: NSViewController {
12 |
13 | @IBOutlet weak var appIdTextView: NSTextField!
14 | @IBOutlet weak var appIdTextField: NSTextField!
15 | @IBOutlet weak var spasswordLbl: NSTextField!
16 | @IBOutlet weak var submitBtn: NSButton!
17 |
18 | //通过外界传入的 apple id时,不需要用户填写
19 | var apple_id: String? {
20 | didSet {
21 | if let appId = apple_id {
22 | appIdTextView.stringValue = appId
23 | appIdTextView.isHidden = false
24 | appIdTextField.isHidden = true
25 | } else {
26 | appIdTextView.stringValue = ""
27 | appIdTextView.isHidden = true
28 | appIdTextField.isHidden = false
29 | }
30 | }
31 | }
32 |
33 | private var ipaFileURL: URL?
34 | private var fileDropZoneView = DropZoneView(fileTypes: [".ipa"], text: "点击或拖拽IPA到这里")
35 | private var uploadModel = XMLModel()
36 |
37 | override func viewDidLoad() {
38 | super.viewDidLoad()
39 | setupUI()
40 | updateSPasswordUI()
41 | }
42 |
43 | func setupUI() {
44 |
45 | fileDropZoneView.translatesAutoresizingMaskIntoConstraints = false
46 | fileDropZoneView.delegate = self
47 | view.addSubview(fileDropZoneView)
48 | fileDropZoneView.snp.makeConstraints { (make) in
49 | make.top.equalTo(submitBtn.snp.bottom).offset(15)
50 | make.left.equalToSuperview().offset(20)
51 | make.right.equalToSuperview().offset(-20)
52 | make.bottom.equalToSuperview().offset(-30)
53 | }
54 | }
55 |
56 | func updateSPasswordUI() {
57 | if let sp = UserCenter.shared.currentSPassword {
58 | spasswordLbl.stringValue = "(当前选择:\(sp.account))"
59 | } else {
60 | spasswordLbl.stringValue = "(错误:当前未指定专用密码!)"
61 | }
62 | }
63 |
64 | @IBAction func clickedSPasswordBtn(_ sender: NSButton) {
65 | let vc = APSPasswordSettingVC()
66 | vc.updateCompletion = { [weak self] ps in
67 | self?.updateSPasswordUI()
68 | }
69 | presentAsSheet(vc)
70 | }
71 |
72 | @IBAction func clickedSubmitBtn(_ sender: NSButton) {
73 | uploadIpaFile()
74 | }
75 |
76 | }
77 |
78 | // MARK: - Private Method
79 | extension APIPAUploadVC {
80 |
81 | private func uploadIpaFile() {
82 |
83 | var appId = appIdTextField.stringValue
84 | if let appleId = apple_id {
85 | appId = appleId
86 | }
87 | guard !appId.isEmpty else {
88 | APHUD.hide(message: "请先填写 app id ~", delayTime: 1)
89 | return
90 | }
91 |
92 | guard let sp = UserCenter.shared.currentSPassword else {
93 | let vc = APSPasswordSettingVC()
94 | vc.updateCompletion = { [weak self] spassword in
95 | self?.uploadIpaFile()
96 | }
97 | presentAsSheet(vc)
98 | APHUD.hide(message: "请先设置或指定专用密码~", delayTime: 1)
99 | return
100 | }
101 |
102 | guard let ipaFileURL = ipaFileURL else {
103 | APHUD.hide(message: "请先上传 ipa 文件~", delayTime: 1)
104 | return
105 | }
106 |
107 | APHUD.show(message: "上传中", view: self.view)
108 |
109 | DispatchQueue.global(qos: .userInitiated).async { [self] in
110 |
111 | uploadModel = XMLModel()
112 | uploadModel.apple_id = appId
113 | uploadModel.ipa_size = ipaFileURL.fileSize()
114 | uploadModel.ipa_md5 = ipaFileURL.fileMD5() ?? ""
115 | uploadModel.filePaths = ["ipa.ipa": ipaFileURL.path]
116 |
117 | // 获取创建 itms 文件的路径
118 | let filePath = XMLManager.getIpaPath(appId)
119 |
120 | // 先删除旧的文档
121 | XMLManager.deleteITMS(filePath)
122 |
123 | uploadModel.createIpaFile(directoryPath: filePath)
124 |
125 | let result = XMLManager.uploadITMS(account: sp.account, pwd: sp.password, filePath: filePath)
126 |
127 | DispatchQueue.main.async {
128 | APHUD.hide()
129 | self.closeSelfAndCallBack(result)
130 | }
131 | }
132 |
133 | }
134 |
135 |
136 | func closeSelfAndCallBack(_ result: (Int32, String?)) {
137 | if result.0 == 0 {
138 | NSAlert.show("ipa文件上传成功!稍后可在苹果后台查看~")
139 | }else {
140 | let sb = NSStoryboard(name: "APDebugVC", bundle: Bundle(for: self.classForCoder))
141 | let newWC = sb.instantiateController(withIdentifier: "APDebugWC") as? NSWindowController
142 | let logVC = newWC?.contentViewController as? APDebugVC
143 | newWC?.window?.title = "ipa上传错误日志"
144 | logVC?.debugLog = result.1 ?? ""
145 | newWC?.showWindow(self)
146 | }
147 | }
148 | }
149 |
150 |
151 | // MARK: - DropZoneViewDelegate
152 | extension APIPAUploadVC: DropZoneViewDelegate {
153 |
154 | func receivedFile(dropZoneView: DropZoneView, fileURL: URL) {
155 | ipaFileURL = fileURL
156 | }
157 |
158 | func receivedMouseDown(dropZoneView: DropZoneView, theEvent: NSEvent) {
159 | let openPanel = NSOpenPanel()
160 | openPanel.canChooseFiles = true
161 | openPanel.canChooseDirectories = false
162 | openPanel.allowsMultipleSelection = false
163 | openPanel.allowedFileTypes = ["ipa"]
164 |
165 | openPanel.beginSheetModal(for: self.view.window!) { (modalResponse) in
166 | if modalResponse == .OK {
167 | if let fileURL = openPanel.url {
168 | self.ipaFileURL = fileURL
169 | dropZoneView.setFile(fileURL)
170 | }
171 | }
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/AppleParty/Shared/UI/APSPasswordSettingVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APSPasswordSettingVC.swift
3 | // AppleParty
4 | //
5 | // Created by HTC on 2022/5/18.
6 | // Copyright © 2022 37 Mobile models. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class APSPasswordSettingVC: NSViewController {
12 |
13 | // 模型
14 | var models = [SPassword]()
15 | // 回调当前选择的账号
16 | var updateCompletion: ((_ model: SPassword?) -> Void)?
17 |
18 | @IBOutlet weak var tableView: NSTableView!
19 |
20 |
21 | @IBAction func clickedAddBtn(_ sender: Any) {
22 | let vc = APSPasswordEditVC()
23 | vc.titleString = "新增专用密码"
24 | vc.updateCompletion = { [weak self] news in
25 | // 相同账号的只保留最新
26 | self?.models = self?.models.filter({ $0.account != news.account }) ?? []
27 | if news.isused {
28 | // 只能有一个是使用的账号,其它为否
29 | self?.models = self?.models.map({ sp in
30 | var spp = sp
31 | return spp.model(sp, false)
32 | }) ?? []
33 | }
34 | self?.models.append(news)
35 | self?.tableView.reloadData()
36 | }
37 | presentAsSheet(vc)
38 | }
39 |
40 | @IBAction func clickedSaveBtn(_ sender: Any) {
41 |
42 | guard models.isNotEmpty else {
43 | APHUD.hide(message: "账号邮箱和专用密码不能为空!", view: view, delayTime: 2)
44 | return
45 | }
46 |
47 | // 保存数据
48 | UserCenter.shared.secondaryPasswordList = models
49 |
50 | // 回调当前选择的账号
51 | if let block = updateCompletion {
52 | let models = self.models.filter({ $0.isused == true })
53 | block(models.first)
54 | }
55 | dismiss(self)
56 | }
57 |
58 | @IBAction func clickedCancelBtn(_ sender: Any) {
59 | dismiss(self)
60 | }
61 |
62 | private lazy var editMenu: NSMenu = {
63 | let menu = NSMenu()
64 | let saveItem = NSMenuItem()
65 | saveItem.title = "修改"
66 | saveItem.target = self
67 | saveItem.action = #selector(tableViewEditItemClicked)
68 | menu.addItem(saveItem)
69 | let removeItem = NSMenuItem()
70 | removeItem.title = "删除"
71 | removeItem.target = self
72 | removeItem.action = #selector(tableViewDeleteItemClicked)
73 | menu.addItem(removeItem)
74 | return menu
75 | }()
76 |
77 | @objc private func tableViewEditItemClicked(_ sender: AnyObject) {
78 | let row = tableView.clickedRow
79 | guard row >= 0 else { return }
80 |
81 | let result = models
82 | let index = result.index(result.startIndex, offsetBy: row)
83 | let model = result[index]
84 | let vc = APSPasswordEditVC()
85 | vc.titleString = "新增专用密码"
86 | vc.spassword = model
87 | vc.updateCompletion = { [weak self] news in
88 | if news.isused {
89 | // 只能有一个是使用的账号,其它为否
90 | self?.models = self?.models.map({ sp in
91 | var spp = sp
92 | return spp.model(sp, false)
93 | }) ?? []
94 | }
95 | self?.models[index] = news
96 | self?.tableView.reloadData()
97 | }
98 | presentAsSheet(vc)
99 | }
100 |
101 | @objc private func tableViewDeleteItemClicked(_ sender: AnyObject) {
102 | let row = tableView.clickedRow
103 | guard row >= 0 else { return }
104 |
105 | let result = models
106 | let index = result.index(result.startIndex, offsetBy: row)
107 | models.remove(at: index)
108 | tableView.reloadData()
109 | }
110 |
111 | override func viewDidLoad() {
112 | super.viewDidLoad()
113 | setupUI()
114 | }
115 |
116 | func setupUI() {
117 |
118 | tableView.menu = editMenu
119 | tableView.delegate = self
120 | tableView.dataSource = self
121 |
122 | models = UserCenter.shared.secondaryPasswordList
123 | tableView.reloadData()
124 | }
125 | }
126 |
127 |
128 | // MARK: NSTableViewDataSource && NSTableViewDelegate
129 | extension APSPasswordSettingVC: NSTableViewDataSource, NSTableViewDelegate {
130 | func numberOfRows(in tableView: NSTableView) -> Int {
131 | return models.count
132 | }
133 |
134 | func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
135 | return 30.0
136 | }
137 |
138 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
139 | let result = models
140 | let index = result.index(result.startIndex, offsetBy: row)
141 | let model = result[index]
142 | let identifier = tableColumn!.identifier
143 | let identifierString = identifier.rawValue
144 |
145 | if identifierString == "AccountCell" {
146 | let cellView = tableView.makeView(withIdentifier: identifier, owner: self) as! NSTableCellView
147 | cellView.textField!.stringValue = model.account
148 | return cellView
149 | }
150 | else if identifierString == "PasswordCell" {
151 | let cellView = tableView.makeView(withIdentifier: identifier, owner: self) as! NSTableCellView
152 | cellView.textField!.stringValue = model.password
153 | return cellView
154 | }
155 | else if identifierString == "currentUseCell" {
156 | let cellView = tableView.makeView(withIdentifier: identifier, owner: self) as! NSTableCellView
157 | cellView.textField!.stringValue = model.isused ? "✓" : "-"
158 | return cellView
159 | }
160 | else {
161 | print("unhandled colum id: \(identifierString)")
162 | }
163 | return nil
164 | }
165 |
166 |
167 | // MARK: 是否可以选中单元格
168 | func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
169 |
170 | return true
171 | }
172 |
173 | func tableViewSelectionDidChange(_ notification: Notification) {
174 | let table = notification.object as! NSTableView
175 | table.deselectRow(table.selectedRow)
176 | }
177 |
178 | }
179 |
--------------------------------------------------------------------------------
/AppleParty/AppListView/APAppListCell.xib:
--------------------------------------------------------------------------------
1 |
2 |