diff --git a/.gitignore b/.gitignore index 3542e5b..56620a1 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,8 @@ playground.xcworkspace # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # -# Pods/ +Pods/ +Podfile.lock # # Add this line if you want to avoid checking in source code from the Xcode workspace # *.xcworkspace diff --git a/AppleParty/.github/ISSUE_TEMPLATE/bug_report.md b/AppleParty/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..1567503 --- /dev/null +++ b/AppleParty/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Version** + - OS: + - AppleParty: diff --git a/AppleParty/.github/ISSUE_TEMPLATE/feature_request.md b/AppleParty/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..93c4953 --- /dev/null +++ b/AppleParty/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Tell us how we can improve AppleParty** + +**Is your feature request related to a problem? Please describe.** + +**What would you like to see? How would you like it to work?** diff --git a/AppleParty/.gitignore b/AppleParty/.gitignore new file mode 100644 index 0000000..d8c50d0 --- /dev/null +++ b/AppleParty/.gitignore @@ -0,0 +1,99 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +Pods/ + + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + + +## Other +*.xcuserstate + +## Obj-C/Swift specific +*.hmap +*.ipa +*.swp +.DS_Store \ No newline at end of file diff --git a/AppleParty/AppleParty.xcodeproj/project.pbxproj b/AppleParty/AppleParty.xcodeproj/project.pbxproj new file mode 100644 index 0000000..07b2691 --- /dev/null +++ b/AppleParty/AppleParty.xcodeproj/project.pbxproj @@ -0,0 +1,1368 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 35249A13BD02FE59DD5561A9 /* Pods_AppleParty_ApplePartyUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6CE565FD4718E20367C0FBF5 /* Pods_AppleParty_ApplePartyUITests.framework */; }; + 6D09315A2957154B00AE66EF /* IAPExcelParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D0931582957154B00AE66EF /* IAPExcelParser.swift */; }; + 6D09315E295715DD00AE66EF /* APUploadIAPListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D09315D295715DD00AE66EF /* APUploadIAPListVC.swift */; }; + 6D0931602957160B00AE66EF /* AppStoreConnectAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D09315F2957160B00AE66EF /* AppStoreConnectAPI.swift */; }; + 6D0931652957312C00AE66EF /* APASCKeysEditVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6D0931612957312C00AE66EF /* APASCKeysEditVC.xib */; }; + 6D0931662957312C00AE66EF /* APASCKeysEditVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D0931622957312C00AE66EF /* APASCKeysEditVC.swift */; }; + 6D0931672957312C00AE66EF /* APASCKeysSettingVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6D0931632957312C00AE66EF /* APASCKeysSettingVC.xib */; }; + 6D0931682957312C00AE66EF /* APASCKeysSettingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D0931642957312C00AE66EF /* APASCKeysSettingVC.swift */; }; + 6D09316D295731D000AE66EF /* example.xlsx in Resources */ = {isa = PBXBuildFile; fileRef = 6D09316B295731D000AE66EF /* example.xlsx */; }; + 6D093171295732E000AE66EF /* IPAUpload.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6D09316F295732E000AE66EF /* IPAUpload.storyboard */; }; + 6D093172295732E000AE66EF /* APIPAUploadVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D093170295732E000AE66EF /* APIPAUploadVC.swift */; }; + 6D0931772957345C00AE66EF /* APSPasswordSettingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D0931732957345C00AE66EF /* APSPasswordSettingVC.swift */; }; + 6D0931782957345C00AE66EF /* APSPasswordEditVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6D0931742957345C00AE66EF /* APSPasswordEditVC.xib */; }; + 6D0931792957345C00AE66EF /* APSPasswordEditVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D0931752957345C00AE66EF /* APSPasswordEditVC.swift */; }; + 6D09317A2957345C00AE66EF /* APSPasswordSettingVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6D0931762957345C00AE66EF /* APSPasswordSettingVC.xib */; }; + 6D09317F295736D000AE66EF /* AppStoreConnect-Swift-SDK in Frameworks */ = {isa = PBXBuildFile; productRef = 6D09317E295736D000AE66EF /* AppStoreConnect-Swift-SDK */; }; + 6D36DE4827DAFACD00BBC931 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D36DE4727DAFACD00BBC931 /* AppDelegate.swift */; }; + 6D36DE4C27DAFACE00BBC931 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6D36DE4B27DAFACE00BBC931 /* Assets.xcassets */; }; + 6D36DE5A27DAFACE00BBC931 /* ApplePartyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D36DE5927DAFACE00BBC931 /* ApplePartyTests.swift */; }; + 6D36DE6427DAFACE00BBC931 /* ApplePartyUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D36DE6327DAFACE00BBC931 /* ApplePartyUITests.swift */; }; + 6D36DE6627DAFACE00BBC931 /* ApplePartyUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D36DE6527DAFACE00BBC931 /* ApplePartyUITestsLaunchTests.swift */; }; + 6D36DE7C27DB775800BBC931 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6D36DE7E27DB775800BBC931 /* InfoPlist.strings */; }; + 6D36DE8527DB77C600BBC931 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6D36DE8727DB77C600BBC931 /* Localizable.strings */; }; + 6D3ECCC927F16145005E4597 /* APHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D3ECCC827F16145005E4597 /* APHUD.swift */; }; + 6D3ECCCC27F193AF005E4597 /* PhoneNumbers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D3ECCCB27F193AF005E4597 /* PhoneNumbers.swift */; }; + 6D3ECCCF27F1A9FE005E4597 /* APSwichAccountPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D3ECCCD27F1A9FE005E4597 /* APSwichAccountPopover.swift */; }; + 6D3ECCD027F1A9FE005E4597 /* APSwichAccountPopover.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6D3ECCCE27F1A9FE005E4597 /* APSwichAccountPopover.xib */; }; + 6D3ECCD727F1CAD6005E4597 /* ScreenShotHelpPopoverVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D3ECCD227F1CAD6005E4597 /* ScreenShotHelpPopoverVC.swift */; }; + 6D3ECCD827F1CAD6005E4597 /* ScreenShotHelpPopoverVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6D3ECCD327F1CAD6005E4597 /* ScreenShotHelpPopoverVC.xib */; }; + 6D3ECCD927F1CAD6005E4597 /* ScreenShotUpload.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6D3ECCD427F1CAD6005E4597 /* ScreenShotUpload.storyboard */; }; + 6D3ECCDA27F1CAD6005E4597 /* ScreenShotUploadVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D3ECCD527F1CAD6005E4597 /* ScreenShotUploadVC.swift */; }; + 6D3ECCDB27F1CAD6005E4597 /* ScreenShotUploadCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D3ECCD627F1CAD6005E4597 /* ScreenShotUploadCell.swift */; }; + 6D3ECCDE27F1D027005E4597 /* XMLModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D3ECCDD27F1D027005E4597 /* XMLModel.swift */; }; + 6D3ECCE027F1D2A0005E4597 /* APDebugVC.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6D3ECCDF27F1D2A0005E4597 /* APDebugVC.storyboard */; }; + 6D3ECCE227F1D322005E4597 /* APDebugVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D3ECCE127F1D322005E4597 /* APDebugVC.swift */; }; + 6D3ECCE827F1D6BB005E4597 /* IAPModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D3ECCE727F1D6BB005E4597 /* IAPModel.swift */; }; + 6D3ECCEC27F1E97F005E4597 /* APInAppPurchseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D3ECCEB27F1E97F005E4597 /* APInAppPurchseVC.swift */; }; + 6D3ECCEE27F1E989005E4597 /* APInAppPurchseVC.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6D3ECCED27F1E989005E4597 /* APInAppPurchseVC.storyboard */; }; + 6D584B5627F20A4D00924BFE /* APInappPurchseCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6D584B5527F20A4D00924BFE /* APInappPurchseCell.xib */; }; + 6D584B5827F20A8000924BFE /* APInAppPurchseCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D584B5727F20A8000924BFE /* APInAppPurchseCell.swift */; }; + 6D59A20B27F29DB600C7D8F5 /* SwiftSMTP in Frameworks */ = {isa = PBXBuildFile; productRef = 6D59A20A27F29DB600C7D8F5 /* SwiftSMTP */; }; + 6D59A20F27F29E8B00C7D8F5 /* EmailTool.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6D59A20C27F29E8B00C7D8F5 /* EmailTool.storyboard */; }; + 6D59A21027F29E8B00C7D8F5 /* EmailToolVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D59A20D27F29E8B00C7D8F5 /* EmailToolVC.swift */; }; + 6D59A21127F29E8B00C7D8F5 /* DropZoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D59A20E27F29E8B00C7D8F5 /* DropZoneView.swift */; }; + 6D59A21327F29F4C00C7D8F5 /* EmailUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D59A21227F29F4C00C7D8F5 /* EmailUtils.swift */; }; + 6D59A21527F2C46100C7D8F5 /* InAppPurchseView.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6D59A21427F2C46100C7D8F5 /* InAppPurchseView.storyboard */; }; + 6D59A21927F2D3C600C7D8F5 /* IAPUploadImageVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D59A21827F2D3C600C7D8F5 /* IAPUploadImageVC.swift */; }; + 6D59A21B27F2D49000C7D8F5 /* DragView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D59A21A27F2D49000C7D8F5 /* DragView.swift */; }; + 6D59A21D27F2D77700C7D8F5 /* OutputExcelVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D59A21C27F2D77700C7D8F5 /* OutputExcelVC.swift */; }; + 6D59A22127F3055300C7D8F5 /* APQRcodeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D59A22027F3055300C7D8F5 /* APQRcodeVC.swift */; }; + 6D59A22327F3056B00C7D8F5 /* APQRcode.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6D59A22227F3056B00C7D8F5 /* APQRcode.storyboard */; }; + 6D59A22627F306A900C7D8F5 /* QrcodeUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D59A22527F306A900C7D8F5 /* QrcodeUtil.m */; }; + 6D59A22F27F31E7000C7D8F5 /* APSettingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D59A22D27F31E7000C7D8F5 /* APSettingVC.swift */; }; + 6D59A23027F31E7000C7D8F5 /* APSettingVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6D59A22E27F31E7000C7D8F5 /* APSettingVC.xib */; }; + 6D59A23A27F3395900C7D8F5 /* EmailSettingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D59A23827F3395900C7D8F5 /* EmailSettingVC.swift */; }; + 6D59A23B27F3395A00C7D8F5 /* EmailSettingVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6D59A23927F3395900C7D8F5 /* EmailSettingVC.xib */; }; + 6D59A24727FE833800C7D8F5 /* APConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D59A24627FE833800C7D8F5 /* APConstants.swift */; }; + 6D7065B8298A544800031916 /* MBProgressHUD.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D7065B6298A544800031916 /* MBProgressHUD.m */; }; + 6D8F184A27F07E97001A30BF /* APAppListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F182D27F07E97001A30BF /* APAppListVC.swift */; }; + 6D8F184D27F07E97001A30BF /* APRootVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F183927F07E97001A30BF /* APRootVC.swift */; }; + 6D8F184F27F07E97001A30BF /* APRootWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F183D27F07E97001A30BF /* APRootWC.swift */; }; + 6D8F185027F07E97001A30BF /* APRootCollectionCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6D8F183E27F07E97001A30BF /* APRootCollectionCell.xib */; }; + 6D8F185127F07E97001A30BF /* APRootCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F183F27F07E97001A30BF /* APRootCollectionCell.swift */; }; + 6D8F185227F07E97001A30BF /* APRootCollectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F184027F07E97001A30BF /* APRootCollectionModel.swift */; }; + 6D8F185327F07E97001A30BF /* APRootCollectionAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F184127F07E97001A30BF /* APRootCollectionAdapter.swift */; }; + 6D8F185427F07E97001A30BF /* UIExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F184427F07E97001A30BF /* UIExtension.swift */; }; + 6D8F185527F07E97001A30BF /* APCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F184527F07E97001A30BF /* APCollectionView.swift */; }; + 6D8F185627F07E97001A30BF /* APUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F184827F07E97001A30BF /* APUtil.swift */; }; + 6D8F185927F07F4B001A30BF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6D8F185727F07F4B001A30BF /* Main.storyboard */; }; + 6D8F185C27F08161001A30BF /* AppList.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6D8F185E27F08161001A30BF /* AppList.storyboard */; }; + 6D8F186327F081D3001A30BF /* APAppListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F186127F081D3001A30BF /* APAppListCell.swift */; }; + 6D8F186427F081D3001A30BF /* APAppListCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6D8F186227F081D3001A30BF /* APAppListCell.xib */; }; + 6D8F186627F081ED001A30BF /* APAppListAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F186527F081ED001A30BF /* APAppListAdapter.swift */; }; + 6D8F186827F08201001A30BF /* APAppListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F186727F08201001A30BF /* APAppListModel.swift */; }; + 6D8F186F27F08F65001A30BF /* APLoginVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F186D27F08F65001A30BF /* APLoginVC.swift */; }; + 6D8F187027F08F65001A30BF /* APLoginVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6D8F186E27F08F65001A30BF /* APLoginVC.xib */; }; + 6D8F187327F0A070001A30BF /* UserCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F187227F0A070001A30BF /* UserCenter.swift */; }; + 6D8F187527F0A0B3001A30BF /* InfoCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F187427F0A0B3001A30BF /* InfoCenter.swift */; }; + 6D8F187827F0A2DD001A30BF /* APLogin2FAVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F187627F0A2DD001A30BF /* APLogin2FAVC.swift */; }; + 6D8F187927F0A2DD001A30BF /* APLogin2FAVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6D8F187727F0A2DD001A30BF /* APLogin2FAVC.xib */; }; + 6D8F188427F0AC83001A30BF /* XMLManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F188027F0AC83001A30BF /* XMLManager.swift */; }; + 6D8F188827F0ACA5001A30BF /* GDataXMLNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F188727F0ACA5001A30BF /* GDataXMLNode.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 6D8F188A27F0ADC2001A30BF /* FoundationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F188927F0ADC2001A30BF /* FoundationUtil.swift */; }; + 6D8F188E27F0C121001A30BF /* APClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F188D27F0C121001A30BF /* APClient.swift */; }; + 6D8F189027F0C3CB001A30BF /* ARLogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F188F27F0C3CB001A30BF /* ARLogs.swift */; }; + 6DA62AB029FBD1AC0093E1C2 /* APVerifyReceipt.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6DA62AAE29FBD1AC0093E1C2 /* APVerifyReceipt.storyboard */; }; + 6DA62AB129FBD1AC0093E1C2 /* APVerifyReceiptVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA62AAF29FBD1AC0093E1C2 /* APVerifyReceiptVC.swift */; }; + 6DC6B0852CD0E6FB00E4C0D7 /* AppleWebLoginCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC6B0832CD0E6FB00E4C0D7 /* AppleWebLoginCore.swift */; }; + 6DC6B0882CD0E70900E4C0D7 /* APWebLoginVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC6B0862CD0E70900E4C0D7 /* APWebLoginVC.swift */; }; + 6DC6B0892CD0E70900E4C0D7 /* APWebLoginVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6DC6B0872CD0E70900E4C0D7 /* APWebLoginVC.xib */; }; + 814E3C3BB5538B3ED396D7DF /* Pods_ApplePartyTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 46A4FD1F5C0341D48CF4D106 /* Pods_ApplePartyTests.framework */; }; + BA7B4461DBCFDE92C6C0DDA8 /* Pods_AppleParty.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8DDC8A0206F410C17653E3A /* Pods_AppleParty.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 6D36DE5627DAFACE00BBC931 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6D36DE3C27DAFACD00BBC931 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6D36DE4327DAFACD00BBC931; + remoteInfo = AppleParty; + }; + 6D36DE6027DAFACE00BBC931 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6D36DE3C27DAFACD00BBC931 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6D36DE4327DAFACD00BBC931; + remoteInfo = AppleParty; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 1D3D597799DB4DF597EDC003 /* Pods-ApplePartyTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ApplePartyTests.debug.xcconfig"; path = "Target Support Files/Pods-ApplePartyTests/Pods-ApplePartyTests.debug.xcconfig"; sourceTree = ""; }; + 4140E6B7075C708364368BBC /* Pods-AppleParty-ApplePartyUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppleParty-ApplePartyUITests.release.xcconfig"; path = "Target Support Files/Pods-AppleParty-ApplePartyUITests/Pods-AppleParty-ApplePartyUITests.release.xcconfig"; sourceTree = ""; }; + 45F1FE2A4C13D7D378FD2C07 /* Pods-ApplePartyTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ApplePartyTests.release.xcconfig"; path = "Target Support Files/Pods-ApplePartyTests/Pods-ApplePartyTests.release.xcconfig"; sourceTree = ""; }; + 46A4FD1F5C0341D48CF4D106 /* Pods_ApplePartyTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ApplePartyTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 526A86591D63E15605829ECE /* Pods-AppleParty.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppleParty.release.xcconfig"; path = "Target Support Files/Pods-AppleParty/Pods-AppleParty.release.xcconfig"; sourceTree = ""; }; + 63F4DCD655332D4AA5E2ECF5 /* Pods-AppleParty-ApplePartyUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppleParty-ApplePartyUITests.debug.xcconfig"; path = "Target Support Files/Pods-AppleParty-ApplePartyUITests/Pods-AppleParty-ApplePartyUITests.debug.xcconfig"; sourceTree = ""; }; + 6CE565FD4718E20367C0FBF5 /* Pods_AppleParty_ApplePartyUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AppleParty_ApplePartyUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6D0931582957154B00AE66EF /* IAPExcelParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IAPExcelParser.swift; sourceTree = ""; }; + 6D09315D295715DD00AE66EF /* APUploadIAPListVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APUploadIAPListVC.swift; sourceTree = ""; }; + 6D09315F2957160B00AE66EF /* AppStoreConnectAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStoreConnectAPI.swift; sourceTree = ""; }; + 6D0931612957312C00AE66EF /* APASCKeysEditVC.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = APASCKeysEditVC.xib; sourceTree = ""; }; + 6D0931622957312C00AE66EF /* APASCKeysEditVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APASCKeysEditVC.swift; sourceTree = ""; }; + 6D0931632957312C00AE66EF /* APASCKeysSettingVC.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = APASCKeysSettingVC.xib; sourceTree = ""; }; + 6D0931642957312C00AE66EF /* APASCKeysSettingVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APASCKeysSettingVC.swift; sourceTree = ""; }; + 6D09316B295731D000AE66EF /* example.xlsx */ = {isa = PBXFileReference; lastKnownFileType = file; path = example.xlsx; sourceTree = ""; }; + 6D09316F295732E000AE66EF /* IPAUpload.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = IPAUpload.storyboard; sourceTree = ""; }; + 6D093170295732E000AE66EF /* APIPAUploadVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIPAUploadVC.swift; sourceTree = ""; }; + 6D0931732957345C00AE66EF /* APSPasswordSettingVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APSPasswordSettingVC.swift; sourceTree = ""; }; + 6D0931742957345C00AE66EF /* APSPasswordEditVC.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = APSPasswordEditVC.xib; sourceTree = ""; }; + 6D0931752957345C00AE66EF /* APSPasswordEditVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APSPasswordEditVC.swift; sourceTree = ""; }; + 6D0931762957345C00AE66EF /* APSPasswordSettingVC.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = APSPasswordSettingVC.xib; sourceTree = ""; }; + 6D09317B295735D100AE66EF /* ipa_metadata.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = ipa_metadata.xml; sourceTree = ""; }; + 6D36DE4427DAFACD00BBC931 /* AppleParty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppleParty.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 6D36DE4727DAFACD00BBC931 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 6D36DE4B27DAFACE00BBC931 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 6D36DE5527DAFACE00BBC931 /* ApplePartyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ApplePartyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 6D36DE5927DAFACE00BBC931 /* ApplePartyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePartyTests.swift; sourceTree = ""; }; + 6D36DE5F27DAFACE00BBC931 /* ApplePartyUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ApplePartyUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 6D36DE6327DAFACE00BBC931 /* ApplePartyUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePartyUITests.swift; sourceTree = ""; }; + 6D36DE6527DAFACE00BBC931 /* ApplePartyUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePartyUITestsLaunchTests.swift; sourceTree = ""; }; + 6D36DE7327DAFB5F00BBC931 /* AppleParty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = AppleParty.entitlements; path = AppleParty/AppleParty.entitlements; sourceTree = ""; }; + 6D36DE7427DAFB6C00BBC931 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 6D36DE7D27DB775800BBC931 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; + 6D36DE8027DB777400BBC931 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 6D36DE8627DB77C600BBC931 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 6D36DE8827DB77C700BBC931 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + 6D36DE8927DEEA6E00BBC931 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = AppleParty/Info.plist; sourceTree = ""; }; + 6D3ECCC827F16145005E4597 /* APHUD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APHUD.swift; sourceTree = ""; }; + 6D3ECCCB27F193AF005E4597 /* PhoneNumbers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhoneNumbers.swift; sourceTree = ""; }; + 6D3ECCCD27F1A9FE005E4597 /* APSwichAccountPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APSwichAccountPopover.swift; sourceTree = ""; }; + 6D3ECCCE27F1A9FE005E4597 /* APSwichAccountPopover.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = APSwichAccountPopover.xib; sourceTree = ""; }; + 6D3ECCD227F1CAD6005E4597 /* ScreenShotHelpPopoverVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenShotHelpPopoverVC.swift; sourceTree = ""; }; + 6D3ECCD327F1CAD6005E4597 /* ScreenShotHelpPopoverVC.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ScreenShotHelpPopoverVC.xib; sourceTree = ""; }; + 6D3ECCD427F1CAD6005E4597 /* ScreenShotUpload.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ScreenShotUpload.storyboard; sourceTree = ""; }; + 6D3ECCD527F1CAD6005E4597 /* ScreenShotUploadVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenShotUploadVC.swift; sourceTree = ""; }; + 6D3ECCD627F1CAD6005E4597 /* ScreenShotUploadCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenShotUploadCell.swift; sourceTree = ""; }; + 6D3ECCDD27F1D027005E4597 /* XMLModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLModel.swift; sourceTree = ""; }; + 6D3ECCDF27F1D2A0005E4597 /* APDebugVC.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = APDebugVC.storyboard; sourceTree = ""; }; + 6D3ECCE127F1D322005E4597 /* APDebugVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APDebugVC.swift; sourceTree = ""; }; + 6D3ECCE727F1D6BB005E4597 /* IAPModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IAPModel.swift; sourceTree = ""; }; + 6D3ECCEB27F1E97F005E4597 /* APInAppPurchseVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APInAppPurchseVC.swift; sourceTree = ""; }; + 6D3ECCED27F1E989005E4597 /* APInAppPurchseVC.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = APInAppPurchseVC.storyboard; sourceTree = ""; }; + 6D3ECCF027F1ECE2005E4597 /* iap_metadata.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = iap_metadata.xml; sourceTree = ""; }; + 6D3ECCF227F1ECF8005E4597 /* shot_metadata.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = shot_metadata.xml; sourceTree = ""; }; + 6D584B5527F20A4D00924BFE /* APInappPurchseCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = APInappPurchseCell.xib; sourceTree = ""; }; + 6D584B5727F20A8000924BFE /* APInAppPurchseCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APInAppPurchseCell.swift; sourceTree = ""; }; + 6D59A20C27F29E8B00C7D8F5 /* EmailTool.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = EmailTool.storyboard; sourceTree = ""; }; + 6D59A20D27F29E8B00C7D8F5 /* EmailToolVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailToolVC.swift; sourceTree = ""; }; + 6D59A20E27F29E8B00C7D8F5 /* DropZoneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropZoneView.swift; sourceTree = ""; }; + 6D59A21227F29F4C00C7D8F5 /* EmailUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailUtils.swift; sourceTree = ""; }; + 6D59A21427F2C46100C7D8F5 /* InAppPurchseView.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = InAppPurchseView.storyboard; sourceTree = ""; }; + 6D59A21827F2D3C600C7D8F5 /* IAPUploadImageVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IAPUploadImageVC.swift; sourceTree = ""; }; + 6D59A21A27F2D49000C7D8F5 /* DragView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragView.swift; sourceTree = ""; }; + 6D59A21C27F2D77700C7D8F5 /* OutputExcelVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutputExcelVC.swift; sourceTree = ""; }; + 6D59A22027F3055300C7D8F5 /* APQRcodeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APQRcodeVC.swift; sourceTree = ""; }; + 6D59A22227F3056B00C7D8F5 /* APQRcode.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = APQRcode.storyboard; sourceTree = ""; }; + 6D59A22427F306A900C7D8F5 /* QrcodeUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QrcodeUtil.h; sourceTree = ""; }; + 6D59A22527F306A900C7D8F5 /* QrcodeUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QrcodeUtil.m; sourceTree = ""; }; + 6D59A22D27F31E7000C7D8F5 /* APSettingVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APSettingVC.swift; sourceTree = ""; }; + 6D59A22E27F31E7000C7D8F5 /* APSettingVC.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = APSettingVC.xib; sourceTree = ""; }; + 6D59A23327F321BA00C7D8F5 /* update.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = update.xml; sourceTree = ""; }; + 6D59A23527F321CA00C7D8F5 /* AppleParty-release.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "AppleParty-release.html"; sourceTree = ""; }; + 6D59A23827F3395900C7D8F5 /* EmailSettingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailSettingVC.swift; sourceTree = ""; }; + 6D59A23927F3395900C7D8F5 /* EmailSettingVC.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EmailSettingVC.xib; sourceTree = ""; }; + 6D59A24627FE833800C7D8F5 /* APConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APConstants.swift; sourceTree = ""; }; + 6D7065B6298A544800031916 /* MBProgressHUD.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MBProgressHUD.m; sourceTree = ""; }; + 6D7065B7298A544800031916 /* MBProgressHUD.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MBProgressHUD.h; sourceTree = ""; }; + 6D8F182D27F07E97001A30BF /* APAppListVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APAppListVC.swift; sourceTree = ""; }; + 6D8F183927F07E97001A30BF /* APRootVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APRootVC.swift; sourceTree = ""; }; + 6D8F183D27F07E97001A30BF /* APRootWC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APRootWC.swift; sourceTree = ""; }; + 6D8F183E27F07E97001A30BF /* APRootCollectionCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = APRootCollectionCell.xib; sourceTree = ""; }; + 6D8F183F27F07E97001A30BF /* APRootCollectionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APRootCollectionCell.swift; sourceTree = ""; }; + 6D8F184027F07E97001A30BF /* APRootCollectionModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APRootCollectionModel.swift; sourceTree = ""; }; + 6D8F184127F07E97001A30BF /* APRootCollectionAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APRootCollectionAdapter.swift; sourceTree = ""; }; + 6D8F184427F07E97001A30BF /* UIExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIExtension.swift; sourceTree = ""; }; + 6D8F184527F07E97001A30BF /* APCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APCollectionView.swift; sourceTree = ""; }; + 6D8F184827F07E97001A30BF /* APUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APUtil.swift; sourceTree = ""; }; + 6D8F185827F07F4B001A30BF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 6D8F185B27F07F58001A30BF /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; + 6D8F185D27F08161001A30BF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/AppList.storyboard; sourceTree = ""; }; + 6D8F186027F08162001A30BF /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/AppList.strings"; sourceTree = ""; }; + 6D8F186127F081D3001A30BF /* APAppListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APAppListCell.swift; sourceTree = ""; }; + 6D8F186227F081D3001A30BF /* APAppListCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = APAppListCell.xib; sourceTree = ""; }; + 6D8F186527F081ED001A30BF /* APAppListAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APAppListAdapter.swift; sourceTree = ""; }; + 6D8F186727F08201001A30BF /* APAppListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APAppListModel.swift; sourceTree = ""; }; + 6D8F186D27F08F65001A30BF /* APLoginVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APLoginVC.swift; sourceTree = ""; }; + 6D8F186E27F08F65001A30BF /* APLoginVC.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = APLoginVC.xib; sourceTree = ""; }; + 6D8F187227F0A070001A30BF /* UserCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCenter.swift; sourceTree = ""; }; + 6D8F187427F0A0B3001A30BF /* InfoCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoCenter.swift; sourceTree = ""; }; + 6D8F187627F0A2DD001A30BF /* APLogin2FAVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APLogin2FAVC.swift; sourceTree = ""; }; + 6D8F187727F0A2DD001A30BF /* APLogin2FAVC.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = APLogin2FAVC.xib; sourceTree = ""; }; + 6D8F188027F0AC83001A30BF /* XMLManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLManager.swift; sourceTree = ""; }; + 6D8F188527F0ACA4001A30BF /* AppleParty-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AppleParty-Bridging-Header.h"; sourceTree = ""; }; + 6D8F188627F0ACA5001A30BF /* GDataXMLNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GDataXMLNode.h; sourceTree = ""; }; + 6D8F188727F0ACA5001A30BF /* GDataXMLNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GDataXMLNode.m; sourceTree = ""; }; + 6D8F188927F0ADC2001A30BF /* FoundationUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoundationUtil.swift; sourceTree = ""; }; + 6D8F188D27F0C121001A30BF /* APClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APClient.swift; sourceTree = ""; }; + 6D8F188F27F0C3CB001A30BF /* ARLogs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARLogs.swift; sourceTree = ""; }; + 6DA62AAE29FBD1AC0093E1C2 /* APVerifyReceipt.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = APVerifyReceipt.storyboard; sourceTree = ""; }; + 6DA62AAF29FBD1AC0093E1C2 /* APVerifyReceiptVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APVerifyReceiptVC.swift; sourceTree = ""; }; + 6DC6B0832CD0E6FB00E4C0D7 /* AppleWebLoginCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWebLoginCore.swift; sourceTree = ""; }; + 6DC6B0862CD0E70900E4C0D7 /* APWebLoginVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APWebLoginVC.swift; sourceTree = ""; }; + 6DC6B0872CD0E70900E4C0D7 /* APWebLoginVC.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = APWebLoginVC.xib; sourceTree = ""; }; + 6E2329117B30DC9F72DC1E3E /* Pods-AppleParty.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppleParty.debug.xcconfig"; path = "Target Support Files/Pods-AppleParty/Pods-AppleParty.debug.xcconfig"; sourceTree = ""; }; + F8DDC8A0206F410C17653E3A /* Pods_AppleParty.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AppleParty.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 6D36DE4127DAFACD00BBC931 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6D59A20B27F29DB600C7D8F5 /* SwiftSMTP in Frameworks */, + BA7B4461DBCFDE92C6C0DDA8 /* Pods_AppleParty.framework in Frameworks */, + 6D09317F295736D000AE66EF /* AppStoreConnect-Swift-SDK in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6D36DE5227DAFACE00BBC931 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 814E3C3BB5538B3ED396D7DF /* Pods_ApplePartyTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6D36DE5C27DAFACE00BBC931 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 35249A13BD02FE59DD5561A9 /* Pods_AppleParty_ApplePartyUITests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 263330C868F2CFAF4C8A67BC /* Pods */ = { + isa = PBXGroup; + children = ( + 6E2329117B30DC9F72DC1E3E /* Pods-AppleParty.debug.xcconfig */, + 526A86591D63E15605829ECE /* Pods-AppleParty.release.xcconfig */, + 63F4DCD655332D4AA5E2ECF5 /* Pods-AppleParty-ApplePartyUITests.debug.xcconfig */, + 4140E6B7075C708364368BBC /* Pods-AppleParty-ApplePartyUITests.release.xcconfig */, + 1D3D597799DB4DF597EDC003 /* Pods-ApplePartyTests.debug.xcconfig */, + 45F1FE2A4C13D7D378FD2C07 /* Pods-ApplePartyTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 6D093169295731D000AE66EF /* InAppPurchase */ = { + isa = PBXGroup; + children = ( + 6D09316B295731D000AE66EF /* example.xlsx */, + ); + path = InAppPurchase; + sourceTree = ""; + }; + 6D09316E295732D100AE66EF /* IPAUpload */ = { + isa = PBXGroup; + children = ( + 6D093170295732E000AE66EF /* APIPAUploadVC.swift */, + 6D09316F295732E000AE66EF /* IPAUpload.storyboard */, + ); + path = IPAUpload; + sourceTree = ""; + }; + 6D36DE3B27DAFACD00BBC931 = { + isa = PBXGroup; + children = ( + 6D36DE7427DAFB6C00BBC931 /* README.md */, + 6D36DE7327DAFB5F00BBC931 /* AppleParty.entitlements */, + 6D36DE8927DEEA6E00BBC931 /* Info.plist */, + 6D36DE7E27DB775800BBC931 /* InfoPlist.strings */, + 6D36DE8727DB77C600BBC931 /* Localizable.strings */, + 6D36DE4627DAFACD00BBC931 /* AppleParty */, + 6D36DE5827DAFACE00BBC931 /* ApplePartyTests */, + 6D36DE6227DAFACE00BBC931 /* ApplePartyUITests */, + 6D36DE4527DAFACD00BBC931 /* Products */, + 263330C868F2CFAF4C8A67BC /* Pods */, + BD41E9D291F096DAD93AE347 /* Frameworks */, + ); + sourceTree = ""; + }; + 6D36DE4527DAFACD00BBC931 /* Products */ = { + isa = PBXGroup; + children = ( + 6D36DE4427DAFACD00BBC931 /* AppleParty.app */, + 6D36DE5527DAFACE00BBC931 /* ApplePartyTests.xctest */, + 6D36DE5F27DAFACE00BBC931 /* ApplePartyUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 6D36DE4627DAFACD00BBC931 /* AppleParty */ = { + isa = PBXGroup; + children = ( + 6D36DE4727DAFACD00BBC931 /* AppDelegate.swift */, + 6D8F188527F0ACA4001A30BF /* AppleParty-Bridging-Header.h */, + 6D8F183627F07E97001A30BF /* RootView */, + 6D36DE7527DAFCD900BBC931 /* LoginView */, + 6D8F182B27F07E97001A30BF /* AppListView */, + 6D8F183027F07E97001A30BF /* EmailToolView */, + 6D09316E295732D100AE66EF /* IPAUpload */, + 6D8F183427F07E97001A30BF /* QRcodeView */, + 6DA62AAD29FBD19C0093E1C2 /* VerifyReceipt */, + 6D8F183127F07E97001A30BF /* AppSettingView */, + 6D8F184227F07E97001A30BF /* Shared */, + 6D8F187A27F0AC74001A30BF /* Vendors */, + 6D8F183327F07E97001A30BF /* SparkleUpdate */, + 6D36DE7627DAFCFD00BBC931 /* Resources */, + ); + path = AppleParty; + sourceTree = ""; + }; + 6D36DE5827DAFACE00BBC931 /* ApplePartyTests */ = { + isa = PBXGroup; + children = ( + 6D36DE5927DAFACE00BBC931 /* ApplePartyTests.swift */, + ); + path = ApplePartyTests; + sourceTree = ""; + }; + 6D36DE6227DAFACE00BBC931 /* ApplePartyUITests */ = { + isa = PBXGroup; + children = ( + 6D36DE6327DAFACE00BBC931 /* ApplePartyUITests.swift */, + 6D36DE6527DAFACE00BBC931 /* ApplePartyUITestsLaunchTests.swift */, + ); + path = ApplePartyUITests; + sourceTree = ""; + }; + 6D36DE7527DAFCD900BBC931 /* LoginView */ = { + isa = PBXGroup; + children = ( + 6DC6B0862CD0E70900E4C0D7 /* APWebLoginVC.swift */, + 6DC6B0872CD0E70900E4C0D7 /* APWebLoginVC.xib */, + 6D8F186D27F08F65001A30BF /* APLoginVC.swift */, + 6D8F186E27F08F65001A30BF /* APLoginVC.xib */, + 6D8F187627F0A2DD001A30BF /* APLogin2FAVC.swift */, + 6D8F187727F0A2DD001A30BF /* APLogin2FAVC.xib */, + 6D3ECCCB27F193AF005E4597 /* PhoneNumbers.swift */, + 6DC6B0842CD0E6FB00E4C0D7 /* AppleWebLogin */, + ); + path = LoginView; + sourceTree = ""; + }; + 6D36DE7627DAFCFD00BBC931 /* Resources */ = { + isa = PBXGroup; + children = ( + 6D36DE4B27DAFACE00BBC931 /* Assets.xcassets */, + 6D093169295731D000AE66EF /* InAppPurchase */, + 6D3ECCEF27F1ECD3005E4597 /* Transporter */, + ); + path = Resources; + sourceTree = ""; + }; + 6D3ECCD127F1CAC5005E4597 /* ScreenShotsView */ = { + isa = PBXGroup; + children = ( + 6D3ECCD527F1CAD6005E4597 /* ScreenShotUploadVC.swift */, + 6D3ECCD427F1CAD6005E4597 /* ScreenShotUpload.storyboard */, + 6D3ECCD627F1CAD6005E4597 /* ScreenShotUploadCell.swift */, + 6D3ECCD227F1CAD6005E4597 /* ScreenShotHelpPopoverVC.swift */, + 6D3ECCD327F1CAD6005E4597 /* ScreenShotHelpPopoverVC.xib */, + ); + path = ScreenShotsView; + sourceTree = ""; + }; + 6D3ECCDC27F1D00B005E4597 /* Models */ = { + isa = PBXGroup; + children = ( + 6D3ECCE727F1D6BB005E4597 /* IAPModel.swift */, + 6D3ECCDD27F1D027005E4597 /* XMLModel.swift */, + 6D0931582957154B00AE66EF /* IAPExcelParser.swift */, + ); + path = Models; + sourceTree = ""; + }; + 6D3ECCEF27F1ECD3005E4597 /* Transporter */ = { + isa = PBXGroup; + children = ( + 6D3ECCF227F1ECF8005E4597 /* shot_metadata.xml */, + 6D3ECCF027F1ECE2005E4597 /* iap_metadata.xml */, + 6D09317B295735D100AE66EF /* ipa_metadata.xml */, + ); + path = Transporter; + sourceTree = ""; + }; + 6D7065B5298A544800031916 /* MBProgressHUD-OSX */ = { + isa = PBXGroup; + children = ( + 6D7065B7298A544800031916 /* MBProgressHUD.h */, + 6D7065B6298A544800031916 /* MBProgressHUD.m */, + ); + path = "MBProgressHUD-OSX"; + sourceTree = ""; + }; + 6D8F182B27F07E97001A30BF /* AppListView */ = { + isa = PBXGroup; + children = ( + 6D8F182D27F07E97001A30BF /* APAppListVC.swift */, + 6D8F185E27F08161001A30BF /* AppList.storyboard */, + 6D8F186527F081ED001A30BF /* APAppListAdapter.swift */, + 6D8F186127F081D3001A30BF /* APAppListCell.swift */, + 6D8F186227F081D3001A30BF /* APAppListCell.xib */, + 6D8F186727F08201001A30BF /* APAppListModel.swift */, + 6D8F183227F07E97001A30BF /* InAppPurchseView */, + 6D3ECCD127F1CAC5005E4597 /* ScreenShotsView */, + ); + path = AppListView; + sourceTree = ""; + }; + 6D8F183027F07E97001A30BF /* EmailToolView */ = { + isa = PBXGroup; + children = ( + 6D59A20D27F29E8B00C7D8F5 /* EmailToolVC.swift */, + 6D59A20C27F29E8B00C7D8F5 /* EmailTool.storyboard */, + 6D59A23827F3395900C7D8F5 /* EmailSettingVC.swift */, + 6D59A23927F3395900C7D8F5 /* EmailSettingVC.xib */, + ); + path = EmailToolView; + sourceTree = ""; + }; + 6D8F183127F07E97001A30BF /* AppSettingView */ = { + isa = PBXGroup; + children = ( + 6D59A22D27F31E7000C7D8F5 /* APSettingVC.swift */, + 6D59A22E27F31E7000C7D8F5 /* APSettingVC.xib */, + ); + path = AppSettingView; + sourceTree = ""; + }; + 6D8F183227F07E97001A30BF /* InAppPurchseView */ = { + isa = PBXGroup; + children = ( + 6D3ECCEB27F1E97F005E4597 /* APInAppPurchseVC.swift */, + 6D3ECCED27F1E989005E4597 /* APInAppPurchseVC.storyboard */, + 6D09315D295715DD00AE66EF /* APUploadIAPListVC.swift */, + 6D59A21827F2D3C600C7D8F5 /* IAPUploadImageVC.swift */, + 6D59A21C27F2D77700C7D8F5 /* OutputExcelVC.swift */, + 6D59A21427F2C46100C7D8F5 /* InAppPurchseView.storyboard */, + 6D584B5727F20A8000924BFE /* APInAppPurchseCell.swift */, + 6D584B5527F20A4D00924BFE /* APInappPurchseCell.xib */, + 6D59A21A27F2D49000C7D8F5 /* DragView.swift */, + 6D3ECCDC27F1D00B005E4597 /* Models */, + ); + path = InAppPurchseView; + sourceTree = ""; + }; + 6D8F183327F07E97001A30BF /* SparkleUpdate */ = { + isa = PBXGroup; + children = ( + 6D59A23327F321BA00C7D8F5 /* update.xml */, + 6D59A23527F321CA00C7D8F5 /* AppleParty-release.html */, + ); + path = SparkleUpdate; + sourceTree = ""; + }; + 6D8F183427F07E97001A30BF /* QRcodeView */ = { + isa = PBXGroup; + children = ( + 6D59A22027F3055300C7D8F5 /* APQRcodeVC.swift */, + 6D59A22227F3056B00C7D8F5 /* APQRcode.storyboard */, + ); + path = QRcodeView; + sourceTree = ""; + }; + 6D8F183627F07E97001A30BF /* RootView */ = { + isa = PBXGroup; + children = ( + 6D8F185727F07F4B001A30BF /* Main.storyboard */, + 6D8F183D27F07E97001A30BF /* APRootWC.swift */, + 6D8F183927F07E97001A30BF /* APRootVC.swift */, + 6D8F183F27F07E97001A30BF /* APRootCollectionCell.swift */, + 6D8F183E27F07E97001A30BF /* APRootCollectionCell.xib */, + 6D8F184027F07E97001A30BF /* APRootCollectionModel.swift */, + 6D8F184127F07E97001A30BF /* APRootCollectionAdapter.swift */, + 6D3ECCCD27F1A9FE005E4597 /* APSwichAccountPopover.swift */, + 6D3ECCCE27F1A9FE005E4597 /* APSwichAccountPopover.xib */, + ); + path = RootView; + sourceTree = ""; + }; + 6D8F184227F07E97001A30BF /* Shared */ = { + isa = PBXGroup; + children = ( + 6D8F184627F07E97001A30BF /* Network */, + 6D8F187127F0A028001A30BF /* Info */, + 6D8F184327F07E97001A30BF /* UI */, + 6D8F184727F07E97001A30BF /* Utils */, + ); + path = Shared; + sourceTree = ""; + }; + 6D8F184327F07E97001A30BF /* UI */ = { + isa = PBXGroup; + children = ( + 6D59A20E27F29E8B00C7D8F5 /* DropZoneView.swift */, + 6D8F184427F07E97001A30BF /* UIExtension.swift */, + 6D8F184527F07E97001A30BF /* APCollectionView.swift */, + 6D3ECCE127F1D322005E4597 /* APDebugVC.swift */, + 6D3ECCDF27F1D2A0005E4597 /* APDebugVC.storyboard */, + 6D0931752957345C00AE66EF /* APSPasswordEditVC.swift */, + 6D0931742957345C00AE66EF /* APSPasswordEditVC.xib */, + 6D0931732957345C00AE66EF /* APSPasswordSettingVC.swift */, + 6D0931762957345C00AE66EF /* APSPasswordSettingVC.xib */, + 6D0931622957312C00AE66EF /* APASCKeysEditVC.swift */, + 6D0931612957312C00AE66EF /* APASCKeysEditVC.xib */, + 6D0931642957312C00AE66EF /* APASCKeysSettingVC.swift */, + 6D0931632957312C00AE66EF /* APASCKeysSettingVC.xib */, + ); + path = UI; + sourceTree = ""; + }; + 6D8F184627F07E97001A30BF /* Network */ = { + isa = PBXGroup; + children = ( + 6D09315F2957160B00AE66EF /* AppStoreConnectAPI.swift */, + 6D8F188D27F0C121001A30BF /* APClient.swift */, + ); + path = Network; + sourceTree = ""; + }; + 6D8F184727F07E97001A30BF /* Utils */ = { + isa = PBXGroup; + children = ( + 6D59A21227F29F4C00C7D8F5 /* EmailUtils.swift */, + 6D3ECCC827F16145005E4597 /* APHUD.swift */, + 6D8F188F27F0C3CB001A30BF /* ARLogs.swift */, + 6D8F184827F07E97001A30BF /* APUtil.swift */, + 6D8F188927F0ADC2001A30BF /* FoundationUtil.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 6D8F187127F0A028001A30BF /* Info */ = { + isa = PBXGroup; + children = ( + 6D8F187227F0A070001A30BF /* UserCenter.swift */, + 6D8F187427F0A0B3001A30BF /* InfoCenter.swift */, + 6D59A24627FE833800C7D8F5 /* APConstants.swift */, + ); + path = Info; + sourceTree = ""; + }; + 6D8F187A27F0AC74001A30BF /* Vendors */ = { + isa = PBXGroup; + children = ( + 6D59A22427F306A900C7D8F5 /* QrcodeUtil.h */, + 6D59A22527F306A900C7D8F5 /* QrcodeUtil.m */, + 6D8F187B27F0AC83001A30BF /* ITMS */, + 6D7065B5298A544800031916 /* MBProgressHUD-OSX */, + ); + path = Vendors; + sourceTree = ""; + }; + 6D8F187B27F0AC83001A30BF /* ITMS */ = { + isa = PBXGroup; + children = ( + 6D8F188627F0ACA5001A30BF /* GDataXMLNode.h */, + 6D8F188727F0ACA5001A30BF /* GDataXMLNode.m */, + 6D8F188027F0AC83001A30BF /* XMLManager.swift */, + ); + path = ITMS; + sourceTree = ""; + }; + 6DA62AAD29FBD19C0093E1C2 /* VerifyReceipt */ = { + isa = PBXGroup; + children = ( + 6DA62AAF29FBD1AC0093E1C2 /* APVerifyReceiptVC.swift */, + 6DA62AAE29FBD1AC0093E1C2 /* APVerifyReceipt.storyboard */, + ); + path = VerifyReceipt; + sourceTree = ""; + }; + 6DC6B0842CD0E6FB00E4C0D7 /* AppleWebLogin */ = { + isa = PBXGroup; + children = ( + 6DC6B0832CD0E6FB00E4C0D7 /* AppleWebLoginCore.swift */, + ); + path = AppleWebLogin; + sourceTree = ""; + }; + BD41E9D291F096DAD93AE347 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F8DDC8A0206F410C17653E3A /* Pods_AppleParty.framework */, + 6CE565FD4718E20367C0FBF5 /* Pods_AppleParty_ApplePartyUITests.framework */, + 46A4FD1F5C0341D48CF4D106 /* Pods_ApplePartyTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 6D36DE4327DAFACD00BBC931 /* AppleParty */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6D36DE6927DAFACE00BBC931 /* Build configuration list for PBXNativeTarget "AppleParty" */; + buildPhases = ( + EF99C81E73B67DBE97EBBA39 /* [CP] Check Pods Manifest.lock */, + 6D36DE4027DAFACD00BBC931 /* Sources */, + 6D36DE4127DAFACD00BBC931 /* Frameworks */, + 6D36DE4227DAFACD00BBC931 /* Resources */, + 85A7106372C39938C2F612EE /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AppleParty; + packageProductDependencies = ( + 6D59A20A27F29DB600C7D8F5 /* SwiftSMTP */, + 6D09317E295736D000AE66EF /* AppStoreConnect-Swift-SDK */, + ); + productName = AppleParty; + productReference = 6D36DE4427DAFACD00BBC931 /* AppleParty.app */; + productType = "com.apple.product-type.application"; + }; + 6D36DE5427DAFACE00BBC931 /* ApplePartyTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6D36DE6C27DAFACE00BBC931 /* Build configuration list for PBXNativeTarget "ApplePartyTests" */; + buildPhases = ( + B16E5E88FC9AC7770A136D10 /* [CP] Check Pods Manifest.lock */, + 6D36DE5127DAFACE00BBC931 /* Sources */, + 6D36DE5227DAFACE00BBC931 /* Frameworks */, + 6D36DE5327DAFACE00BBC931 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6D36DE5727DAFACE00BBC931 /* PBXTargetDependency */, + ); + name = ApplePartyTests; + productName = ApplePartyTests; + productReference = 6D36DE5527DAFACE00BBC931 /* ApplePartyTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 6D36DE5E27DAFACE00BBC931 /* ApplePartyUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6D36DE6F27DAFACE00BBC931 /* Build configuration list for PBXNativeTarget "ApplePartyUITests" */; + buildPhases = ( + 72A6F575AC1CEEC6E868DAD7 /* [CP] Check Pods Manifest.lock */, + 6D36DE5B27DAFACE00BBC931 /* Sources */, + 6D36DE5C27DAFACE00BBC931 /* Frameworks */, + 6D36DE5D27DAFACE00BBC931 /* Resources */, + 5DA300C58850B75E97028554 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 6D36DE6127DAFACE00BBC931 /* PBXTargetDependency */, + ); + name = ApplePartyUITests; + productName = ApplePartyUITests; + productReference = 6D36DE5F27DAFACE00BBC931 /* ApplePartyUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 6D36DE3C27DAFACD00BBC931 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + CLASSPREFIX = AP; + LastSwiftUpdateCheck = 1320; + LastUpgradeCheck = 1320; + ORGANIZATIONNAME = "37 Mobile Games"; + TargetAttributes = { + 6D36DE4327DAFACD00BBC931 = { + CreatedOnToolsVersion = 13.2.1; + LastSwiftMigration = 1330; + }; + 6D36DE5427DAFACE00BBC931 = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = 6D36DE4327DAFACD00BBC931; + }; + 6D36DE5E27DAFACE00BBC931 = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = 6D36DE4327DAFACD00BBC931; + }; + }; + }; + buildConfigurationList = 6D36DE3F27DAFACD00BBC931 /* Build configuration list for PBXProject "AppleParty" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + "zh-Hans", + ); + mainGroup = 6D36DE3B27DAFACD00BBC931; + packageReferences = ( + 6D59A20927F29DB600C7D8F5 /* XCRemoteSwiftPackageReference "Swift-SMTP" */, + 6D09317D295736D000AE66EF /* XCRemoteSwiftPackageReference "appstoreconnect-swift-sdk" */, + ); + productRefGroup = 6D36DE4527DAFACD00BBC931 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 6D36DE4327DAFACD00BBC931 /* AppleParty */, + 6D36DE5427DAFACE00BBC931 /* ApplePartyTests */, + 6D36DE5E27DAFACE00BBC931 /* ApplePartyUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 6D36DE4227DAFACD00BBC931 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6D09317A2957345C00AE66EF /* APSPasswordSettingVC.xib in Resources */, + 6D59A21527F2C46100C7D8F5 /* InAppPurchseView.storyboard in Resources */, + 6D8F185C27F08161001A30BF /* AppList.storyboard in Resources */, + 6D8F187027F08F65001A30BF /* APLoginVC.xib in Resources */, + 6D3ECCEE27F1E989005E4597 /* APInAppPurchseVC.storyboard in Resources */, + 6D59A23027F31E7000C7D8F5 /* APSettingVC.xib in Resources */, + 6D8F186427F081D3001A30BF /* APAppListCell.xib in Resources */, + 6D8F187927F0A2DD001A30BF /* APLogin2FAVC.xib in Resources */, + 6D0931652957312C00AE66EF /* APASCKeysEditVC.xib in Resources */, + 6DA62AB029FBD1AC0093E1C2 /* APVerifyReceipt.storyboard in Resources */, + 6D0931782957345C00AE66EF /* APSPasswordEditVC.xib in Resources */, + 6D8F185027F07E97001A30BF /* APRootCollectionCell.xib in Resources */, + 6D3ECCD927F1CAD6005E4597 /* ScreenShotUpload.storyboard in Resources */, + 6D3ECCE027F1D2A0005E4597 /* APDebugVC.storyboard in Resources */, + 6D584B5627F20A4D00924BFE /* APInappPurchseCell.xib in Resources */, + 6D36DE4C27DAFACE00BBC931 /* Assets.xcassets in Resources */, + 6D59A20F27F29E8B00C7D8F5 /* EmailTool.storyboard in Resources */, + 6D59A22327F3056B00C7D8F5 /* APQRcode.storyboard in Resources */, + 6D09316D295731D000AE66EF /* example.xlsx in Resources */, + 6D8F185927F07F4B001A30BF /* Main.storyboard in Resources */, + 6D0931672957312C00AE66EF /* APASCKeysSettingVC.xib in Resources */, + 6D093171295732E000AE66EF /* IPAUpload.storyboard in Resources */, + 6D59A23B27F3395A00C7D8F5 /* EmailSettingVC.xib in Resources */, + 6D3ECCD027F1A9FE005E4597 /* APSwichAccountPopover.xib in Resources */, + 6D36DE8527DB77C600BBC931 /* Localizable.strings in Resources */, + 6D3ECCD827F1CAD6005E4597 /* ScreenShotHelpPopoverVC.xib in Resources */, + 6DC6B0892CD0E70900E4C0D7 /* APWebLoginVC.xib in Resources */, + 6D36DE7C27DB775800BBC931 /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6D36DE5327DAFACE00BBC931 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6D36DE5D27DAFACE00BBC931 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 5DA300C58850B75E97028554 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-AppleParty-ApplePartyUITests/Pods-AppleParty-ApplePartyUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-AppleParty-ApplePartyUITests/Pods-AppleParty-ApplePartyUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AppleParty-ApplePartyUITests/Pods-AppleParty-ApplePartyUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 72A6F575AC1CEEC6E868DAD7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-AppleParty-ApplePartyUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 85A7106372C39938C2F612EE /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-AppleParty/Pods-AppleParty-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-AppleParty/Pods-AppleParty-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AppleParty/Pods-AppleParty-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B16E5E88FC9AC7770A136D10 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ApplePartyTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + EF99C81E73B67DBE97EBBA39 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-AppleParty-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 6D36DE4027DAFACD00BBC931 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6D8F186827F08201001A30BF /* APAppListModel.swift in Sources */, + 6D8F186327F081D3001A30BF /* APAppListCell.swift in Sources */, + 6D3ECCC927F16145005E4597 /* APHUD.swift in Sources */, + 6D59A22F27F31E7000C7D8F5 /* APSettingVC.swift in Sources */, + 6DC6B0852CD0E6FB00E4C0D7 /* AppleWebLoginCore.swift in Sources */, + 6D8F187327F0A070001A30BF /* UserCenter.swift in Sources */, + 6D3ECCEC27F1E97F005E4597 /* APInAppPurchseVC.swift in Sources */, + 6D59A22127F3055300C7D8F5 /* APQRcodeVC.swift in Sources */, + 6D7065B8298A544800031916 /* MBProgressHUD.m in Sources */, + 6D59A21027F29E8B00C7D8F5 /* EmailToolVC.swift in Sources */, + 6D8F185527F07E97001A30BF /* APCollectionView.swift in Sources */, + 6D8F185227F07E97001A30BF /* APRootCollectionModel.swift in Sources */, + 6DA62AB129FBD1AC0093E1C2 /* APVerifyReceiptVC.swift in Sources */, + 6D59A21127F29E8B00C7D8F5 /* DropZoneView.swift in Sources */, + 6D8F185627F07E97001A30BF /* APUtil.swift in Sources */, + 6D09315E295715DD00AE66EF /* APUploadIAPListVC.swift in Sources */, + 6D3ECCD727F1CAD6005E4597 /* ScreenShotHelpPopoverVC.swift in Sources */, + 6D8F188427F0AC83001A30BF /* XMLManager.swift in Sources */, + 6D8F186627F081ED001A30BF /* APAppListAdapter.swift in Sources */, + 6D0931792957345C00AE66EF /* APSPasswordEditVC.swift in Sources */, + 6D584B5827F20A8000924BFE /* APInAppPurchseCell.swift in Sources */, + 6D3ECCDA27F1CAD6005E4597 /* ScreenShotUploadVC.swift in Sources */, + 6D59A24727FE833800C7D8F5 /* APConstants.swift in Sources */, + 6D59A21327F29F4C00C7D8F5 /* EmailUtils.swift in Sources */, + 6D0931602957160B00AE66EF /* AppStoreConnectAPI.swift in Sources */, + 6D3ECCCF27F1A9FE005E4597 /* APSwichAccountPopover.swift in Sources */, + 6D8F188827F0ACA5001A30BF /* GDataXMLNode.m in Sources */, + 6D8F189027F0C3CB001A30BF /* ARLogs.swift in Sources */, + 6D59A21B27F2D49000C7D8F5 /* DragView.swift in Sources */, + 6D3ECCE227F1D322005E4597 /* APDebugVC.swift in Sources */, + 6D3ECCDB27F1CAD6005E4597 /* ScreenShotUploadCell.swift in Sources */, + 6D8F185427F07E97001A30BF /* UIExtension.swift in Sources */, + 6D59A23A27F3395900C7D8F5 /* EmailSettingVC.swift in Sources */, + 6D8F187527F0A0B3001A30BF /* InfoCenter.swift in Sources */, + 6D0931772957345C00AE66EF /* APSPasswordSettingVC.swift in Sources */, + 6D8F186F27F08F65001A30BF /* APLoginVC.swift in Sources */, + 6D36DE4827DAFACD00BBC931 /* AppDelegate.swift in Sources */, + 6D093172295732E000AE66EF /* APIPAUploadVC.swift in Sources */, + 6DC6B0882CD0E70900E4C0D7 /* APWebLoginVC.swift in Sources */, + 6D59A21D27F2D77700C7D8F5 /* OutputExcelVC.swift in Sources */, + 6D3ECCDE27F1D027005E4597 /* XMLModel.swift in Sources */, + 6D8F184D27F07E97001A30BF /* APRootVC.swift in Sources */, + 6D3ECCE827F1D6BB005E4597 /* IAPModel.swift in Sources */, + 6D09315A2957154B00AE66EF /* IAPExcelParser.swift in Sources */, + 6D0931682957312C00AE66EF /* APASCKeysSettingVC.swift in Sources */, + 6D8F185127F07E97001A30BF /* APRootCollectionCell.swift in Sources */, + 6D3ECCCC27F193AF005E4597 /* PhoneNumbers.swift in Sources */, + 6D8F188E27F0C121001A30BF /* APClient.swift in Sources */, + 6D8F187827F0A2DD001A30BF /* APLogin2FAVC.swift in Sources */, + 6D59A21927F2D3C600C7D8F5 /* IAPUploadImageVC.swift in Sources */, + 6D8F184F27F07E97001A30BF /* APRootWC.swift in Sources */, + 6D8F184A27F07E97001A30BF /* APAppListVC.swift in Sources */, + 6D8F188A27F0ADC2001A30BF /* FoundationUtil.swift in Sources */, + 6D0931662957312C00AE66EF /* APASCKeysEditVC.swift in Sources */, + 6D59A22627F306A900C7D8F5 /* QrcodeUtil.m in Sources */, + 6D8F185327F07E97001A30BF /* APRootCollectionAdapter.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6D36DE5127DAFACE00BBC931 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6D36DE5A27DAFACE00BBC931 /* ApplePartyTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6D36DE5B27DAFACE00BBC931 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6D36DE6627DAFACE00BBC931 /* ApplePartyUITestsLaunchTests.swift in Sources */, + 6D36DE6427DAFACE00BBC931 /* ApplePartyUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 6D36DE5727DAFACE00BBC931 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6D36DE4327DAFACD00BBC931 /* AppleParty */; + targetProxy = 6D36DE5627DAFACE00BBC931 /* PBXContainerItemProxy */; + }; + 6D36DE6127DAFACE00BBC931 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6D36DE4327DAFACD00BBC931 /* AppleParty */; + targetProxy = 6D36DE6027DAFACE00BBC931 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 6D36DE7E27DB775800BBC931 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 6D36DE7D27DB775800BBC931 /* zh-Hans */, + 6D36DE8027DB777400BBC931 /* en */, + ); + name = InfoPlist.strings; + path = AppleParty; + sourceTree = ""; + }; + 6D36DE8727DB77C600BBC931 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 6D36DE8627DB77C600BBC931 /* en */, + 6D36DE8827DB77C700BBC931 /* zh-Hans */, + ); + name = Localizable.strings; + path = AppleParty; + sourceTree = ""; + }; + 6D8F185727F07F4B001A30BF /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 6D8F185827F07F4B001A30BF /* Base */, + 6D8F185B27F07F58001A30BF /* zh-Hans */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 6D8F185E27F08161001A30BF /* AppList.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 6D8F185D27F08161001A30BF /* Base */, + 6D8F186027F08162001A30BF /* zh-Hans */, + ); + name = AppList.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 6D36DE6727DAFACE00BBC931 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 6D36DE6827DAFACE00BBC931 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 6D36DE6A27DAFACE00BBC931 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6E2329117B30DC9F72DC1E3E /* Pods-AppleParty.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = AppleParty/AppleParty.entitlements; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 2025.09.29; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AppleParty/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AppleParty; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSHumanReadableCopyright = "37 Mobile Games."; + INFOPLIST_KEY_NSMainStoryboardFile = Main; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 3.8.0; + PRODUCT_BUNDLE_IDENTIFIER = cn.com.37iOS.AppleParty; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "AppleParty/AppleParty-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 6D36DE6B27DAFACE00BBC931 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 526A86591D63E15605829ECE /* Pods-AppleParty.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = AppleParty/AppleParty.entitlements; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 2025.09.29; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AppleParty/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AppleParty; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSHumanReadableCopyright = "37 Mobile Games."; + INFOPLIST_KEY_NSMainStoryboardFile = Main; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 3.8.0; + PRODUCT_BUNDLE_IDENTIFIER = cn.com.37iOS.AppleParty; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "AppleParty/AppleParty-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 6D36DE6D27DAFACE00BBC931 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1D3D597799DB4DF597EDC003 /* Pods-ApplePartyTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.37iOS.ApplePartyTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AppleParty.app/Contents/MacOS/AppleParty"; + }; + name = Debug; + }; + 6D36DE6E27DAFACE00BBC931 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 45F1FE2A4C13D7D378FD2C07 /* Pods-ApplePartyTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.37iOS.ApplePartyTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AppleParty.app/Contents/MacOS/AppleParty"; + }; + name = Release; + }; + 6D36DE7027DAFACE00BBC931 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 63F4DCD655332D4AA5E2ECF5 /* Pods-AppleParty-ApplePartyUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.37iOS.ApplePartyUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = AppleParty; + }; + name = Debug; + }; + 6D36DE7127DAFACE00BBC931 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4140E6B7075C708364368BBC /* Pods-AppleParty-ApplePartyUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.37iOS.ApplePartyUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = AppleParty; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6D36DE3F27DAFACD00BBC931 /* Build configuration list for PBXProject "AppleParty" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6D36DE6727DAFACE00BBC931 /* Debug */, + 6D36DE6827DAFACE00BBC931 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6D36DE6927DAFACE00BBC931 /* Build configuration list for PBXNativeTarget "AppleParty" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6D36DE6A27DAFACE00BBC931 /* Debug */, + 6D36DE6B27DAFACE00BBC931 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6D36DE6C27DAFACE00BBC931 /* Build configuration list for PBXNativeTarget "ApplePartyTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6D36DE6D27DAFACE00BBC931 /* Debug */, + 6D36DE6E27DAFACE00BBC931 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6D36DE6F27DAFACE00BBC931 /* Build configuration list for PBXNativeTarget "ApplePartyUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6D36DE7027DAFACE00BBC931 /* Debug */, + 6D36DE7127DAFACE00BBC931 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 6D09317D295736D000AE66EF /* XCRemoteSwiftPackageReference "appstoreconnect-swift-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/AvdLee/appstoreconnect-swift-sdk.git"; + requirement = { + kind = versionRange; + maximumVersion = 5.0.0; + minimumVersion = 4.0.2; + }; + }; + 6D59A20927F29DB600C7D8F5 /* XCRemoteSwiftPackageReference "Swift-SMTP" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Kitura/Swift-SMTP"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 6D09317E295736D000AE66EF /* AppStoreConnect-Swift-SDK */ = { + isa = XCSwiftPackageProductDependency; + package = 6D09317D295736D000AE66EF /* XCRemoteSwiftPackageReference "appstoreconnect-swift-sdk" */; + productName = "AppStoreConnect-Swift-SDK"; + }; + 6D59A20A27F29DB600C7D8F5 /* SwiftSMTP */ = { + isa = XCSwiftPackageProductDependency; + package = 6D59A20927F29DB600C7D8F5 /* XCRemoteSwiftPackageReference "Swift-SMTP" */; + productName = SwiftSMTP; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 6D36DE3C27DAFACD00BBC931 /* Project object */; +} diff --git a/AppleParty/AppleParty.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/AppleParty/AppleParty.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/AppleParty/AppleParty.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/AppleParty/AppleParty.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/AppleParty/AppleParty.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/AppleParty/AppleParty.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/AppleParty/AppleParty.xcworkspace/contents.xcworkspacedata b/AppleParty/AppleParty.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..cbbfdbf --- /dev/null +++ b/AppleParty/AppleParty.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/AppleParty/AppleParty.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/AppleParty/AppleParty.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/AppleParty/AppleParty.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/AppleParty/AppleParty.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AppleParty/AppleParty.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..72f94e8 --- /dev/null +++ b/AppleParty/AppleParty.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,96 @@ +{ + "originHash" : "9d0d9b5cf3459d54c921a64b1bb7a803edf7e8255bc0e8227a135b3820aeee10", + "pins" : [ + { + "identity" : "appstoreconnect-swift-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/AvdLee/appstoreconnect-swift-sdk.git", + "state" : { + "revision" : "78b2be2f68f30141fca2f7bce45ca7866535cf28", + "version" : "4.0.2" + } + }, + { + "identity" : "bluecryptor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kitura/BlueCryptor.git", + "state" : { + "revision" : "cec97c24b111351e70e448972a7d3fe68a756d6d", + "version" : "2.0.2" + } + }, + { + "identity" : "bluesocket", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kitura/BlueSocket.git", + "state" : { + "revision" : "7b23a867008e0027bfd6f4d398d44720707bc8ca", + "version" : "2.0.4" + } + }, + { + "identity" : "bluesslservice", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kitura/BlueSSLService.git", + "state" : { + "revision" : "b27a94d063962dfa1bba9f79814c4ef202cf33a4", + "version" : "2.0.2" + } + }, + { + "identity" : "loggerapi", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kitura/LoggerAPI.git", + "state" : { + "revision" : "e82d34eab3f0b05391082b11ea07d3b70d2f65bb", + "version" : "1.9.200" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "f70225981241859eb4aa1a18a75531d26637c8cc", + "version" : "1.4.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "95ba0316a9b733e92bb6b071255ff46263bbe7dc", + "version" : "3.15.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c", + "version" : "1.4.4" + } + }, + { + "identity" : "swift-smtp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kitura/Swift-SMTP", + "state" : { + "revision" : "4b7666bb8cee33f0cb367786af17b9a2ebb63047", + "version" : "6.0.0" + } + }, + { + "identity" : "urlqueryencoder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CreateAPI/URLQueryEncoder.git", + "state" : { + "revision" : "4ce950479707ea109f229d7230ec074a133b15d7", + "version" : "0.2.1" + } + } + ], + "version" : 3 +} diff --git a/AppleParty/AppleParty/AppDelegate.swift b/AppleParty/AppleParty/AppDelegate.swift new file mode 100644 index 0000000..59f7045 --- /dev/null +++ b/AppleParty/AppleParty/AppDelegate.swift @@ -0,0 +1,52 @@ +// +// AppDelegate.swift +// AppleParty +// +// Created by HTC on 2022/3/10. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa +import Sparkle + +@main +class AppDelegate: NSObject, NSApplicationDelegate { + + var mainWindow: NSWindow? + @IBOutlet weak var updaterController: SPUStandardUpdaterController! + + func applicationDidFinishLaunching(_ aNotification: Notification) { + // 后台检查更新 + updaterController.updater.checkForUpdatesInBackground() + } + + func applicationWillTerminate(_ aNotification: Notification) { + // Insert code here to tear down your application + } + + func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } + + /// 当关闭最后一个窗口时,退出app + /// - Parameter sender: + /// - Returns: true-窗口程序两者都关闭,false-只关闭窗口 + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + true + } + + /// 应用窗口重新打开时 + /// + /// - Parameters: + /// - sender: + /// - flag: + /// - Returns: + func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + return true + } + + @IBAction func showHelp(_ sender: Any) { + let url = URL(string: kApplePartyWiKi) + NSWorkspace.shared.open(url!) + } +} diff --git a/AppleParty/AppleParty/AppListView/APAppListAdapter.swift b/AppleParty/AppleParty/AppListView/APAppListAdapter.swift new file mode 100644 index 0000000..6ccff5f --- /dev/null +++ b/AppleParty/AppleParty/AppListView/APAppListAdapter.swift @@ -0,0 +1,76 @@ +// +// APAppListAdapter.swift +// AppleParty +// +// Created by HTC on 2022/3/17. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APAppListAdapter: NSObject { + + public var purchseHandle: ((_ app: App) -> Void)? + public var screenshotHandle: ((_ app: App) -> Void)? + + fileprivate static let numberOfSections = 1 + fileprivate static let itemId = "APAppListCell" + + fileprivate var items = [App]() { + didSet { + collectionView.reloadData() + } + } + private var collectionView: NSCollectionView + + init(collectionView: NSCollectionView) { + self.collectionView = collectionView + super.init() + self.collectionView.dataSource = self + self.collectionView.delegate = self + self.collectionView.register(APAppListCell.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: APAppListAdapter.itemId)) + + let itemWidth = CGFloat(350.0) + let itemHeight = CGFloat(150.0) + let itemSpacing = CGFloat(80.0) + let itemPadding = CGFloat(30.0) + + let flowLayout = NSCollectionViewFlowLayout() + flowLayout.scrollDirection = .vertical + flowLayout.itemSize = NSMakeSize(itemWidth, itemHeight) + flowLayout.minimumInteritemSpacing = itemSpacing + flowLayout.minimumLineSpacing = itemSpacing + flowLayout.sectionInset = NSEdgeInsetsMake(itemPadding, itemPadding, itemPadding, itemPadding) + self.collectionView.collectionViewLayout = flowLayout + } + + func set(items: [App]) { + self.items = items + } +} + + +extension APAppListAdapter: NSCollectionViewDataSource, NSCollectionViewDelegate { + func numberOfSectionsInCollectionView(collectionView: NSCollectionView) -> Int { + return APAppListAdapter.numberOfSections + } + + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + return items.count + } + + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { + let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: APAppListAdapter.itemId), for: indexPath) + guard let collectionViewItem = item as? APAppListCell else { return item } + + collectionViewItem.configure(app: items[indexPath.item]) + collectionViewItem.purchseHandle = purchseHandle + collectionViewItem.screenshotHandle = screenshotHandle + + return item + } + + func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { + collectionView.deselectItems(at: indexPaths) + } +} diff --git a/AppleParty/AppleParty/AppListView/APAppListCell.swift b/AppleParty/AppleParty/AppListView/APAppListCell.swift new file mode 100644 index 0000000..22ebddc --- /dev/null +++ b/AppleParty/AppleParty/AppListView/APAppListCell.swift @@ -0,0 +1,44 @@ +// +// APAppListCell.swift +// AppleParty +// +// Created by HTC on 2022/3/17. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APAppListCell: NSCollectionViewItem { + + public var purchseHandle: ((_ app: App) -> Void)? + public var screenshotHandle: ((_ app: App) -> Void)? + + @IBOutlet weak var imgView: NSImageView! + @IBOutlet weak var nameView: NSTextField! + + private var app: App? + + override func viewDidLoad() { + super.viewDidLoad() + nameView.maximumNumberOfLines = 2 + imgView.wantsLayer = true + imgView.layer?.cornerRadius = 22 + imgView.layer?.masksToBounds = true + } + + @IBAction func clickedPurchseItem(_ sender: Any) { + purchseHandle?(app!) + } + + @IBAction func clickedScreenshotItem(_ sender: Any) { + screenshotHandle?(app!) + } + + func configure(app: App) { + self.app = app + nameView.stringValue = app.appName + imgView?.showWebImage(app.iconUrl) + } + + +} diff --git a/AppleParty/AppleParty/AppListView/APAppListCell.xib b/AppleParty/AppleParty/AppListView/APAppListCell.xib new file mode 100644 index 0000000..48d5c8b --- /dev/null +++ b/AppleParty/AppleParty/AppListView/APAppListCell.xib @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/AppListView/APAppListModel.swift b/AppleParty/AppleParty/AppListView/APAppListModel.swift new file mode 100644 index 0000000..a140dfa --- /dev/null +++ b/AppleParty/AppleParty/AppListView/APAppListModel.swift @@ -0,0 +1,83 @@ +// +// APAppListModel.swift +// AppleParty +// +// Created by HTC on 2022/3/17. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Foundation + +// MARK: - 游戏列表 +struct AppList { + var games: [App] + + init(body: [String: Any]) { + games = [App]() + let included = dictionaryArray(body["included"]) + let apps = dictionaryArray(body["data"]) + for software in apps { + var game = App() + let attributes = dictionary(software["attributes"]) + game.appId = string(from: software["id"]) + game.appName = string(from: attributes["name"]) + game.platforms = string(from: attributes["distributionType"]) + game.bundleId = string(from: attributes["bundleId"]) + game.sku = string(from: attributes["sku"]) + game.primaryLocale = string(from: attributes["primaryLocale"]) + // icon 处理 + let appVersion = dictionaryArray( dictionary( dictionary(software["relationships"])["appStoreVersions"])["data"]).first + if let version = appVersion { + let vid = string(from: version["id"]) + for info in included { + let iid = string(from: info["id"]) + if vid == iid, vid.count > 0 { + let info_att = dictionary(info["attributes"]) + let storeIcon = dictionary(info_att["storeIcon"]) + let templateUrl = string(from: storeIcon["templateUrl"]) + if templateUrl.count > 0 { + game.iconUrl = templateUrl.replacingOccurrences(of: "{w}x{h}bb.{f}", with: "500x500bb.png") + } + break + } + } + } + games.append(game) + } + games = games.sorted(by: { (g1, g2) -> Bool in + g1.appName < g2.appName + }) + } +} + +struct App { + var appId: String = "" + var appName: String = "" + var platforms: String = "" + var iconUrl: String = "" + var bundleId: String = "" + var sku: String = "" + var primaryLocale: String = "" +} + +struct AppInfo { + var name: String = "" + var bundleId: String = "" + var bundleIdReferenceName: String = "" + var distributionType: String = "" + var educationDiscountType: String = "" + var sku: String = "" + var primaryLocale: String = "" + + init(body: [String: Any]) { + let data = dictionary(body["data"]) + let attributes = dictionary(data["attributes"]) + name = string(from: attributes["name"]) + bundleId = string(from: attributes["bundleId"]) + bundleIdReferenceName = string(from: attributes["bundleIdReferenceName"]) + distributionType = string(from: attributes["distributionType"]) + educationDiscountType = string(from: attributes["educationDiscountType"]) + sku = string(from: attributes["sku"]) + primaryLocale = string(from: attributes["primaryLocale"]) + } +} diff --git a/AppleParty/AppleParty/AppListView/APAppListVC.swift b/AppleParty/AppleParty/AppListView/APAppListVC.swift new file mode 100644 index 0000000..41ef4c0 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/APAppListVC.swift @@ -0,0 +1,58 @@ +// +// APAppListVC.swift +// AppleParty +// +// Created by HTC on 2022/3/17. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APAppListVC: NSViewController { + + fileprivate var adapter: APAppListAdapter? + + override func viewDidLoad() { + super.viewDidLoad() + configureCollectionView() + fetchAppList() + } + + /// 配置显示的功能列表 + func configureCollectionView() { + let colview = APCollectionView() + colview.configure(superView: view) + adapter = APAppListAdapter(collectionView: colview.collectionView) + adapter?.purchseHandle = { [weak self] app in + let sb = NSStoryboard(name: "APInAppPurchseVC", bundle: nil) + let wc = sb.instantiateController(withIdentifier: "APInAppPurchseVC") as! NSWindowController + let vc = wc.contentViewController as! APInAppPurchseVC + vc.currentApp = app + wc.showWindow(self) + } + adapter?.screenshotHandle = { [weak self] app in + let sb = NSStoryboard(name: "ScreenShotUpload", bundle: nil) + let wc = sb.instantiateController(withIdentifier: "ScreenShotUploadVC") as! NSWindowController + let vc = wc.contentViewController as! ScreenShotUploadVC + vc.currentApp = app + wc.showWindow(self) + } + } + +} + + +// MARK: - 网络请求 +extension APAppListVC { + + func fetchAppList() { + APClient.appList(status: .filter(nil)).request(showLoading: true, inView: self.view) { [weak self] result, response, error in + guard let err = error else { + let gamelist = AppList(body: result) + self?.adapter?.set(items: gamelist.games) + return + } + APHUD.hide(message: err.localizedDescription) + } + } +} diff --git a/AppleParty/AppleParty/AppListView/Base.lproj/AppList.storyboard b/AppleParty/AppleParty/AppListView/Base.lproj/AppList.storyboard new file mode 100644 index 0000000..224c596 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/Base.lproj/AppList.storyboard @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/AppListView/InAppPurchseView/APInAppPurchseCell.swift b/AppleParty/AppleParty/AppListView/InAppPurchseView/APInAppPurchseCell.swift new file mode 100644 index 0000000..4828389 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/InAppPurchseView/APInAppPurchseCell.swift @@ -0,0 +1,158 @@ +// +// APInAppPurchseCell.swift +// AppleParty +// +// Created by HTC on 2022/3/28. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APInAppPurchseCell: NSTableCellView { + + +} + +class ImageViewCell: NSTableCellView { + + @IBOutlet weak var imgSel: NSImageView! + + override func awakeFromNib() { + super.awakeFromNib() + } +} + +class UploadCell: NSTableCellView { + + var row: Int = 0 + + @IBOutlet weak var imgSel: NSImageView! + @IBOutlet weak var dragView: DragView! + @IBOutlet weak var dragBox: NSView! + + typealias CallBackFunc = (_ path: String, _ row: Int) -> Void + var callBackFunc: CallBackFunc? + + override func awakeFromNib() { + super.awakeFromNib() + dragView.delegate = self + } +} + +extension UploadCell: DragViewDelegate { + func dragView(_ path: String?) { + if let path = path { + debugPrint(path) + imgSel.image = NSImage(contentsOfFile: path) + if let callBackFunc = callBackFunc { + callBackFunc(path, row) + } + } else { + imgSel.image = nil + } + } +} + + +enum ColumnIdetifier: String { + case id + case productID + case productName + case priceLevel + case appleid + case price + case type + case state + + // list + case productPds + case level + case status + case screenshot + case language + case upload + case picname + + var columnValue: NSUserInterfaceItemIdentifier { + return NSUserInterfaceItemIdentifier(rawValue: self.rawValue+"Column") + } + var cellValue: NSUserInterfaceItemIdentifier { + return NSUserInterfaceItemIdentifier(rawValue: self.rawValue+"Cell") + } +} + +extension NSUserInterfaceItemIdentifier { + func stringValue() -> String { + switch self { + case ColumnIdetifier.id.columnValue: + return ColumnIdetifier.id.rawValue + case ColumnIdetifier.productID.columnValue: + return ColumnIdetifier.productID.rawValue + case ColumnIdetifier.productName.columnValue: + return ColumnIdetifier.productName.rawValue + case ColumnIdetifier.price.columnValue: + return ColumnIdetifier.price.rawValue + case ColumnIdetifier.type.columnValue: + return ColumnIdetifier.type.rawValue + case ColumnIdetifier.state.columnValue: + return ColumnIdetifier.state.rawValue + case ColumnIdetifier.productPds.columnValue: + return ColumnIdetifier.productPds.rawValue + case ColumnIdetifier.level.columnValue: + return ColumnIdetifier.level.rawValue + case ColumnIdetifier.status.columnValue: + return ColumnIdetifier.status.rawValue + case ColumnIdetifier.appleid.columnValue: + return ColumnIdetifier.appleid.rawValue + case ColumnIdetifier.priceLevel.columnValue: + return ColumnIdetifier.priceLevel.rawValue + case ColumnIdetifier.screenshot.columnValue: + return ColumnIdetifier.screenshot.rawValue + case ColumnIdetifier.picname.columnValue: + return ColumnIdetifier.picname.rawValue + case ColumnIdetifier.upload.columnValue: + return ColumnIdetifier.upload.rawValue + case ColumnIdetifier.language.columnValue: + return ColumnIdetifier.language.rawValue + default: + return "none" + } + } + + func enumValue() -> NSUserInterfaceItemIdentifier { + switch self { + case ColumnIdetifier.id.columnValue: + return ColumnIdetifier.id.cellValue + case ColumnIdetifier.productID.columnValue: + return ColumnIdetifier.productID.cellValue + case ColumnIdetifier.productName.columnValue: + return ColumnIdetifier.productName.cellValue + case ColumnIdetifier.price.columnValue: + return ColumnIdetifier.price.cellValue + case ColumnIdetifier.type.columnValue: + return ColumnIdetifier.type.cellValue + case ColumnIdetifier.state.columnValue: + return ColumnIdetifier.state.cellValue + case ColumnIdetifier.productPds.columnValue: + return ColumnIdetifier.productPds.cellValue + case ColumnIdetifier.level.columnValue: + return ColumnIdetifier.level.cellValue + case ColumnIdetifier.status.columnValue: + return ColumnIdetifier.status.cellValue + case ColumnIdetifier.appleid.columnValue: + return ColumnIdetifier.appleid.cellValue + case ColumnIdetifier.priceLevel.columnValue: + return ColumnIdetifier.priceLevel.cellValue + case ColumnIdetifier.screenshot.columnValue: + return ColumnIdetifier.screenshot.cellValue + case ColumnIdetifier.picname.columnValue: + return ColumnIdetifier.picname.cellValue + case ColumnIdetifier.upload.columnValue: + return ColumnIdetifier.upload.cellValue + case ColumnIdetifier.language.columnValue: + return ColumnIdetifier.language.cellValue + default: + return NSUserInterfaceItemIdentifier(rawValue: "none") + } + } +} diff --git a/AppleParty/AppleParty/AppListView/InAppPurchseView/APInAppPurchseVC.storyboard b/AppleParty/AppleParty/AppListView/InAppPurchseView/APInAppPurchseVC.storyboard new file mode 100644 index 0000000..5ac3eb7 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/InAppPurchseView/APInAppPurchseVC.storyboard @@ -0,0 +1,475 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 6 + 12 + 18 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 6 + 12 + 18 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 6 + 12 + 18 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/AppListView/InAppPurchseView/APInAppPurchseVC.swift b/AppleParty/AppleParty/AppListView/InAppPurchseView/APInAppPurchseVC.swift new file mode 100644 index 0000000..3ecafa3 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/InAppPurchseView/APInAppPurchseVC.swift @@ -0,0 +1,302 @@ +// +// APInAppPurchseVC.swift +// AppleParty +// +// Created by HTC on 2022/3/28. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APInAppPurchseVC: NSViewController { + + public var currentApp: App? { + didSet { + fetchIAPs() + appNameView.stringValue = currentApp?.appName ?? "" + } + } + + var iapList: [IAPList.IAP] = [] + private var countryCode = "" + private var checkPrice = [String: String]() + + @IBOutlet weak var outputIAPListBtn: NSButton! + @IBOutlet weak var appNameView: NSTextField! + @IBOutlet weak var outlineView: NSOutlineView! + + override func viewDidLoad() { + super.viewDidLoad() + self.outlineView.delegate = self + self.outlineView.dataSource = self + self.outlineView.columnAutoresizingStyle = .uniformColumnAutoresizingStyle + self.outlineView.selectionHighlightStyle = .none + self.outlineView.allowsMultipleSelection = true + self.outlineView.sizeToFit() + } + + @IBAction func reloadIAPs(_ sender: Any) { + fetchIAPs() + } + + @IBAction func importExcel(_ sender: Any) { + let openPanel = NSOpenPanel() + openPanel.canChooseFiles = true + openPanel.canChooseDirectories = true + openPanel.allowsMultipleSelection = false + openPanel.beginSheetModal(for: self.view.window!) { [self] (modalResponse: NSApplication.ModalResponse) in + if modalResponse == .OK, let filePath = openPanel.url { + handelExcel(filePath) + } + } + } + + + @IBAction func outputExcel(_ sender: Any) { + + guard iapList.count > 0 else { + NSAlert.show("当前商品为空~") + return + } + + // 创建格式 + var iaps = "productId, 商品名称, 价格等级, 价格(\(countryCode)), AppleID, 商品类型, 状态, 送审图片\n" + let separator = "\",\"" + iaps += iapList.map { item -> String in + return "\"" + item.vendorId + separator + item.referenceName + separator + item.priceTier + separator + (checkPrice[item.priceTier] ?? "-") + separator + item.adamId + + separator + item.addOnType.CNValue() + separator + item.iTunesConnectStatus.statusValue.0 + separator + item.reviewScreenshot + "\"" + }.joined(separator: "\n") + + // 保存文件 + let dateFormatter : DateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMdd_HHmm_" + let currentDate = dateFormatter.string(from: Date()) + let mySave = NSSavePanel() + mySave.allowedFileTypes = ["csv"] + mySave.nameFieldStringValue = "内购列表" + currentDate + currentApp!.appName.replacingOccurrences(of: " ", with: "-") + mySave.begin { (result) -> Void in + if result == .OK { + let filePath = mySave.url + do { + // 含有中文,excel在打开CSV文件时默认用ASNI打开,无BOM头的unicode文件会出现乱码 + var data = Data([0xEF, 0xBB, 0xBF]) + data.append(contentsOf: iaps.data(using: .utf8) ?? Data()) + try data.write(to: filePath!) + } catch { + NSAlert.show("导出失败:\(error.localizedDescription)") + } + } + } + } + + + @IBAction func outputProductID(_ sender: Any) { + let mainStoryboard = NSStoryboard(name: "InAppPurchseView", bundle: Bundle(for: self.classForCoder)) + let outputVC = mainStoryboard.instantiateController(withIdentifier: "OutputExcelVCID") as? OutputExcelVC + outputVC?.iapList = iapList + presentAsSheet(outputVC!) + } + + @IBAction func downloadExcel(_ sender: Any) { + XMLManager.copySimpleExel() + } +} + + +// MARK: - 网络请求 +extension APInAppPurchseVC { + + // 请求商品列表 + func fetchIAPs() { + APClient.iaps(appid: currentApp!.appId).request(showLoading: true, inView: self.view) { [weak self] result, response, error in + guard let err = error else { + guard let app = self?.currentApp else { return } //请求过程关闭页面可能导致为空 + let iapL = IAPList(body:result, app: app) + self?.iapList = iapL.iapList + self?.outlineView.reloadData() + self?.updateRowInfo() + return + } + APHUD.hide(message: err.localizedDescription, view: self?.view ?? currentView()) + } + } + + func updateRowInfo() { + // TODO: 接口请求失败 + return; + + guard self.iapList.count > 0 else { + return + } + + outputIAPListBtn.isEnabled = false + let group = DispatchGroup() + for i in 0.. NSView? { + if let item = item as? IAPList.IAP { + switch tableColumn?.identifier.enumValue() { + case ColumnIdetifier.id.cellValue: + let cell = outlineView.makeView(withIdentifier: ColumnIdetifier.id.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = String(item.curid) + return cell + case ColumnIdetifier.productID.cellValue: + let cell = outlineView.makeView(withIdentifier: ColumnIdetifier.productID.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = item.vendorId + return cell + case ColumnIdetifier.productName.cellValue: + let cell = outlineView.makeView(withIdentifier: ColumnIdetifier.productName.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = item.referenceName + return cell + case ColumnIdetifier.price.cellValue: + let cell = outlineView.makeView(withIdentifier: ColumnIdetifier.price.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = checkPrice[item.priceTier] ?? "-" + return cell + case ColumnIdetifier.priceLevel.cellValue: + let cell = outlineView.makeView(withIdentifier: ColumnIdetifier.priceLevel.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = item.priceTier + return cell + case ColumnIdetifier.appleid.cellValue: + let cell = outlineView.makeView(withIdentifier: ColumnIdetifier.appleid.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = item.adamId + return cell + case ColumnIdetifier.type.cellValue: + let cell = outlineView.makeView(withIdentifier: ColumnIdetifier.type.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = item.addOnType.CNValue() + return cell + case ColumnIdetifier.state.cellValue: + let cell = outlineView.makeView(withIdentifier: ColumnIdetifier.state.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = item.iTunesConnectStatus.statusValue.0 + cell?.textField?.textColor = item.iTunesConnectStatus.statusValue.1 + return cell + default: + return nil + } + }else if let item = item as? String { + switch tableColumn?.identifier.enumValue() { + case ColumnIdetifier.productID.cellValue: + let imgView = NSImageView() + imgView.showWebImage(item) + return imgView + default: + return nil + } + }else { + return nil + } + } + + func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { + if item is IAPList.IAP { + return 1 + }else { + return iapList.count + } + } + + func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { + if item is IAPList.IAP { + return true + }else { + return false + } + } + + func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { + if let item = item as? IAPList.IAP { + return item.reviewScreenshot as Any + } + return iapList[index] + } + + func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat { + if item is IAPList.IAP { + return 35 + }else { + return 100 + } + } +} diff --git a/AppleParty/AppleParty/AppListView/InAppPurchseView/APInappPurchseCell.xib b/AppleParty/AppleParty/AppListView/InAppPurchseView/APInappPurchseCell.xib new file mode 100644 index 0000000..0055c8f --- /dev/null +++ b/AppleParty/AppleParty/AppListView/InAppPurchseView/APInappPurchseCell.xib @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/AppListView/InAppPurchseView/APUploadIAPListVC.swift b/AppleParty/AppleParty/AppListView/InAppPurchseView/APUploadIAPListVC.swift new file mode 100644 index 0000000..a636cd1 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/InAppPurchseView/APUploadIAPListVC.swift @@ -0,0 +1,806 @@ +// +// InputTableListVC.swift +// AppleParty +// +// Created by 易承 on 2020/12/15. +// + +import Cocoa + +class APUploadIAPListVC: NSViewController { + + @IBOutlet weak var tableView: NSTableView! + @IBOutlet weak var enterBtn: NSButton! + @IBOutlet weak var preserveCurrentPriceBtn: NSButton! + @IBOutlet weak var showApiRateLimitLogsBtn: NSButton! + + public var currentApp: App? { + didSet { + setupUI() + } + } + public var iaps = [IAPProduct]() { + didSet { + self.tableView.reloadData() + } + } + + private var screenshotPaths = [String: String]() + + override func viewDidLoad() { + super.viewDidLoad() + + self.tableView.columnAutoresizingStyle = .uniformColumnAutoresizingStyle + self.tableView.selectionHighlightStyle = .none + self.tableView.sizeToFit() + } + + func setupUI() { + self.view.window?.title = "批量内购买项目上传 - " + (currentApp?.appName ?? "") + self.tableView.reloadData() + } + + func showUploadView() { + // 不能同时 present 出来 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + let mainStoryboard = NSStoryboard(name: "InAppPurchseView", bundle: Bundle(for: self.classForCoder)) + let upVC = mainStoryboard.instantiateController(withIdentifier: "IAPUploadVCID") as? IAPUploadImageVC + let screenshot = self.iaps.filter({ $0.reviewScreenshot.count > 0 }).map({ $0.reviewScreenshot }) + // 去重后的图片名 + let uniquedshot = screenshot.enumerated().filter { (index, value) -> Bool in + return screenshot.firstIndex(of: value) == index + }.map { $0.element } + upVC?.picnames = uniquedshot + upVC?.callBackFunc = { paths in + self.screenshotPaths = paths + self.tableView.reloadData() + } + self.presentAsSheet(upVC!) + } + } + + @IBAction func clickedUploadShotBtn(_ sender: Any) { + showUploadView() + } + + @IBAction func clickedSPasswordBtn(_ sender: Any) { + let vc = APASCKeysSettingVC() + presentAsSheet(vc) + } + + @IBAction func createIAP(_ sender: Any) { + let list = self.iaps + guard list.count > 0 else { + APHUD.hide(message: "当前 App 无上传的内购商品!", delayTime: 1) + return + } + + + guard let appid = currentApp?.appId else { + APHUD.hide(message: "当前 App 的 appleid 为空!", delayTime: 1) + return + } + + enterBtn.isEnabled = false + APHUD.show(message: "上传中", view: self.view) + let uploadIAPs: ((AppStoreConnectKey) -> Void) = { [weak self] ascKey in + // 上传数据 + self?.updateInAppPurchse(iaps: list, appId: appid, ascKey: ascKey) + } + + guard let ascKey = InfoCenter.shared.currentASCKey else { + let vc = APASCKeysSettingVC() + vc.updateCompletion = { password in + if let ascKey = password { + uploadIAPs(ascKey) + } + } + presentAsSheet(vc) + return + } + + uploadIAPs(ascKey) + } + +} + +// MARK: - 网络请求 +extension APUploadIAPListVC { + + func updateInAppPurchse(iaps: [IAPProduct], appId: String, ascKey: AppStoreConnectKey) { + let showApiRateLimit = showApiRateLimitLogsBtn.state.rawValue == 1 + let ascAPI = APASCAPI.init(issuerID: ascKey.issuerID, + privateKeyID: ascKey.privateKeyID, + privateKey: ascKey.privateKey, + showApiRateLimit: showApiRateLimit) + ascAPI.addMessage("密钥信息:\(ascKey.issuerID), \(ascKey.privateKeyID), \(ascKey.privateKey)") + + Task { + // 1、获取当前账号下 app,判断是否包含当前 提交商品的 app + guard let apps = await ascAPI.apps() else { + self.enterBtn.isEnabled = true + APHUD.hide() + APHUD.hide(message: "当前请求异常,请检查密钥是否正确~", delayTime: 2) + return + } + let app = apps.filter { $0.id == appId } + guard app.count > 0 else { + self.enterBtn.isEnabled = true + APHUD.hide() + APHUD.hide(message: "当前的密钥没有查到App: \(appId),请检查~", delayTime: 2) + return + } + + // 2. 同步显示进度日志 + let sb = NSStoryboard(name: "APDebugVC", bundle: Bundle(for: self.classForCoder)) + let newWC = sb.instantiateController(withIdentifier: "APDebugWC") as? NSWindowController + newWC?.window?.title = "内购批量上传日志" + let logVC = newWC?.contentViewController as? APDebugVC + logVC?.debugLog = "开始上传" + newWC?.showWindow(self) + ascAPI.updateMsg = { messages in + logVC?.debugLog = messages.joined(separator: "\n") + } + + ascAPI.addMessage("开始处理内购商品,获取现有商品中...") + // 3、获取所有的内购商品,如果存在的商品就直接修改,如果不存在就创建 + let oldIAPs = await ascAPI.fetchInAppPurchasesList(appId: appId) + + // 4、遍历所有要上传的商品 + for product in iaps { + // 订阅类型与非订单类型不一样的处理逻辑 + if product.inAppPurchaseType == .AUTO_RENEWABLE { + await createRenewSubscription(appId: appId, product: product, ascAPI: ascAPI) + } else { + await createInAppPurchase(appId: appId, product: product, oldIAPs: oldIAPs, ascAPI: ascAPI) + } + } + + self.enterBtn.isEnabled = true + APHUD.hide() + ascAPI.addMessage("完成全部内购商品,可稍后在苹果后台查看!✅✅✅") + } + } + + // MARK: - 上传内购类型商品 + + /// 创建内购商品 + func createInAppPurchase(appId: String, product: IAPProduct, oldIAPs: [ASCInAppPurchaseV2], ascAPI: APASCAPI) async { + ascAPI.addMessage("开始上传内购商品:\(product.productId),\(product.name) ") + // 检查是否已经存在此商品,如果存在就修改信息,如果不存在就创建 + let iaps = oldIAPs.filter({ $0.attributes?.productID == product.productId }) + if let iap = iaps.first { + ascAPI.addMessage("内购已经存在:\(product.productId) ,开始更新信息中...") + // 0. 审核备注如果原来有值,而新字段无值,则使用原值 + var product = product + if let note = iap.attributes?.reviewNote, product.reviewNote.isEmpty { + product.reviewNote = note + } + // 1.修改原商品信息 + guard let iap = await ascAPI.updateInAppPurchases(iapId: iap.id, product: product) else { + // 修改失败 + ascAPI.addMessage("内购已经存在:\(product.productId) ,更新信息失败!❌ ") + return + } + // 2. 商品价格档位 + await updateIAPPricePoint(iapId: iap.id, product: product, ascAPI: ascAPI) + + // 3. 商品本地化语言 + ascAPI.addMessage("开始更新内购本地化版本:\(product.productId)") + let localizations = await ascAPI.fetchInAppPurchasesLocalizations(iapId: iap.id) + for localization in product.localizations { + // 如果已经存在本地化语言,则更新 + if let locale = localizations.filter({ $0.attributes?.locale == localization.locale }).first { + // 更新 + ascAPI.addMessage("内购已存在本地化版本:\(localization.locale),开始更新信息中...") + if (await ascAPI.updateInAppPurchasesLocalization(iapLocaleId: locale.id, localization: localization)) != nil { + // 本地化语言更新成功 + ascAPI.addMessage("内购本地化版本:\(localization.locale) ,更新语言成功!✅ ") + } else { + // 本地化语言更新失败 + ascAPI.addMessage("内购本地化版本:\(localization.locale) ,更新语言失败!❌ ") + } + } else { + // 创建 + await createIAPLocalization(iapId: iap.id, localization: localization, product: product, ascAPI: ascAPI) + } + } + + // 4. 商品截图 + await createIAPScreenshot(iapId: iap.id, product: product, ascAPI: ascAPI) + + // 5. 销售国家或地区 + await updateIAPAvailableTerritories(iapId: iap.id, product: product, ascAPI: ascAPI) + + } else { + // 1. 创建新的商品 + guard let iap = await ascAPI.createInAppPurchases(appId: appId, product: product) else { + // 创建失败 + ascAPI.addMessage("内购商品:\(product.productId) ,创建失败!❌ ") + return + } + // 2. 商品价格档位 + await updateIAPPricePoint(iapId: iap.id, product: product, ascAPI: ascAPI) + + // 3. 商品本地化语言 + for localization in product.localizations { + await createIAPLocalization(iapId: iap.id, localization: localization, product: product, ascAPI: ascAPI) + } + + // 4. 商品截图 + await createIAPScreenshot(iapId: iap.id, product: product, ascAPI: ascAPI) + + // 5. 销售国家或地区 + await updateIAPAvailableTerritories(iapId: iap.id, product: product, ascAPI: ascAPI) + } + + ascAPI.addMessage("内购商品:\(product.productId),\(product.name) ,上传完成!\n") + } + + /// 创建内购商品价格档位 + func updateIAPPricePoint(iapId: String, product: IAPProduct, ascAPI: APASCAPI) async { + guard let schedule = product.priceSchedules else { + ascAPI.addMessage("无价格计划表:\(product.productId) ,请确认!❌ ") + return + } + + let baseTerritory = schedule.baseTerritory + let baseCustomerPrice = schedule.baseCustomerPrice.normalizePrice() + + ascAPI.addMessage("开始更新价格计划表:\(product.productId),\(baseTerritory),\(baseCustomerPrice) \n") + + let points = await ascAPI.fetchPricePoints(iapId: iapId, territory: [baseTerritory]) + if let point = points.filter({ $0.attributes?.customerPrice!.normalizePrice() == baseCustomerPrice }).first { + var manualPrices: [Any] = [] + var included: [Any] = [] + + ascAPI.addMessage("开始构建基准国家和自定价格:") + // base Territory + manualPrices.append(["id": "${\(baseTerritory)-\(included.count)}", "type": "inAppPurchasePrices"]) + included.append(ascAPI.fetchInAppPurchasePriceSchedule(scheduleId: baseTerritory, pricePointId: point.id, iapId: iapId, index: included.count)) + + // customerPrice + for pricePoint in schedule.manualPrices { + let territory = pricePoint.territory + let customerPrice = pricePoint.customerPrice.normalizePrice() + let points = await ascAPI.fetchPricePoints(iapId: iapId, territory: [territory]) + if let point = points.filter({ $0.attributes?.customerPrice!.normalizePrice() == customerPrice }).first { + manualPrices.append(["id": "${\(territory)-\(included.count)}", "type": "inAppPurchasePrices"]) + included.append(ascAPI.fetchInAppPurchasePriceSchedule(scheduleId: territory, pricePointId: point.id, iapId: iapId, index: included.count)) + } else { + ascAPI.addMessage("自定价格的内购价格点:\(territory),\(customerPrice) ,未找到此档位!❌ ") + } + } + + ascAPI.saveLogs(log: "内购的基准国家和自定价格:\(manualPrices),\(included)") + + if (await ascAPI.updateInAppPurchasePricePoint(iapId: iapId, baseTerritoryId: baseTerritory, manualPrices: manualPrices, included: included)) != nil { + // 价格档位配置成功 + ascAPI.addMessage("内购价格点:\(baseTerritory),\(baseCustomerPrice) ,更新价格成功!✅ ") + } else { + // 价格档位配置失败 + ascAPI.addMessage("内购价格点:\(baseTerritory),\(baseCustomerPrice) ,更新价格失败!❌ ") + } + } else { + // 找不到价格档位 + ascAPI.addMessage("基准国家的内购价格点:\(baseTerritory),\(baseCustomerPrice) ,未找到此档位!❌ ") + } + } + + + /// 创建内购商品本地化信息 + func createIAPLocalization(iapId: String, localization: IAPLocalization, product: IAPProduct, ascAPI: APASCAPI) async { + ascAPI.addMessage("开始更新本地化版本:\(product.productId),\(localization.locale)") + if (await ascAPI.createInAppPurchasesLocalization(iapId: iapId, localization: localization)) != nil { + // 本地化语言配置成功 + ascAPI.addMessage("内购本地化版本:\(localization.locale) ,更新语言成功!✅ ") + } else { + // 本地化语言配置失败 + ascAPI.addMessage("内购本地化版本:\(localization.locale) ,更新语言失败!❌ ") + } + } + + /// 更新内购商品的送审截图 + func createIAPScreenshot(iapId: String, product: IAPProduct, ascAPI: APASCAPI) async { + ascAPI.addMessage("开始更新内购商品的送审截图:\(product.productId),\(product.reviewScreenshot)") + let imgName = product.reviewScreenshot + guard let imgPath = screenshotPaths[imgName] else { + ascAPI.addMessage("内购商品:\(product.productId) 无送审截图或未上传截图~") + return + } + + let imaUrl = URL.init(fileURLWithPath: imgPath) + guard let fileMD5 = URL.init(fileURLWithPath: imgPath).fileMD5() else { + ascAPI.addMessage("内购商品截图文件错误:\(imgPath) ,无法生成 md5 值~") + return + } + + let oldShot = await ascAPI.fetchInAppPurchasesScreenshot(iapId: iapId) + // 存在需要删除,避免文件名不一样或者过期文件 + if let ost = oldShot { + ascAPI.addMessage("删除旧的送审截图:\(ost.attributes?.fileName ?? "")") + let status = await ascAPI.deleteInAppPurchasesScreenshot(iapShotId: ost.id) + if status != 204 { + ascAPI.addMessage("内购商品截图创建失败:\(imgName) ,无法删除旧截图~") + } + } + + ascAPI.addMessage("创建新的送审截图:\(product.reviewScreenshot)") + // 创建截图 + let imaSize = imaUrl.fileSizeInt() + guard let shot = await ascAPI.createInAppPurchasesScreenshot(iapId: iapId, fileName: imgName, fileSize: imaSize) else { + // 创建失败 + ascAPI.addMessage("内购商品:\(product.productId) ,创建送审截图失败!❌ ") + return + } + + // 根据苹果接口返回的上传接口上传 + guard let method = shot.attributes?.uploadOperations?.first?.method, + let url = shot.attributes?.uploadOperations?.first?.url, + let requestHeaders = shot.attributes?.uploadOperations?.first?.requestHeaders, + let baseURL = URL(string: url) else { + ascAPI.addMessage("内购商品:\(product.productId) ,创建送审截图失败!苹果参数异常~ ❌ ") + return + } + + var request = URLRequest(url: baseURL) + request.httpMethod = method + for header in requestHeaders { + request.headers[header.name ?? ""] = header.value ?? "" + } + + ascAPI.addMessage("上传新的送审截图:\(product.reviewScreenshot)") + // 上传图片 + guard let response = try? await URLSession.shared.upload(for: request, fromFile: imaUrl) else { + ascAPI.addMessage("内购商品:\(product.productId) ,创建送审截图失败!上传图片异常~ ❌ ") + return + } + guard let responseCode = (response.1 as? HTTPURLResponse)?.statusCode, responseCode == 200 else { + ascAPI.addMessage("内购商品:\(product.productId) ,创建送审截图失败!上传图片异常 \(response.1.description)~ ❌ ") + return + } + + ascAPI.addMessage("提交新的送审截图:\(product.reviewScreenshot)") + // 确认图片 + if ((await ascAPI.updateInAppPurchasesScreenshot(iapShotId: shot.id, fileMD5: fileMD5)) != nil) { + ascAPI.addMessage("内购商品:\(product.productId) ,送审截图上传成功!✅ ") + } else { + ascAPI.addMessage("内购商品:\(product.productId) ,送审截图可能上传失败! ") + } + } + + /// 销售国家或地区 + func updateIAPAvailableTerritories(iapId: String, product: IAPProduct, ascAPI: APASCAPI) async { + let inAll = product.territories.availableInAllTerritories + let inNew = product.territories.availableInNewTerritories + let summary = territoryInfo(product: product) + let newTerritory = inNew ? "将来新国家(地区)时自动提供!" : "将来新国家(地区)时不自动提供!" + ascAPI.addMessage("开始更新内购商品的销售国家/地区:\(summary)") + + guard !inAll else { + var allTerritories: [[String: String]] = [] + if let territories = await ascAPI.territories() { + territories.forEach { territory in + allTerritories.append([ + "type": "territories", + "id": territory.id + ]) + } + // 更新全部国家地区 + if (await ascAPI.updateInAppPurchasesAvailabilityTerritories(iapId: iapId, availableTerritories: allTerritories, availableInNewTerritories: inNew)) != nil { + ascAPI.addMessage("选择:所有国家(地区)销售,\(newTerritory),更新成功!✅ ") + } else { + ascAPI.addMessage("选择:所有国家(地区)销售,\(newTerritory),更新失败!❌ ") + } + } else { + ascAPI.addMessage("选择:所有国家(地区)销售,\(newTerritory),无法设置!获取国家标识码失败!❌ ") + } + return + } + + /// 选择销售的国家或地区 + var territories: [[String: String]] = [] + product.territories.territories?.forEach({ territory in + territories.append([ + "type": "territories", + "id": territory.id + ]) + }) + + let customerTerritory = product.territories.territories?.map({ $0.id }).joined(separator: ",") ?? "无" + if (await ascAPI.updateInAppPurchasesAvailabilityTerritories(iapId: iapId, availableTerritories: territories, availableInNewTerritories: inNew)) != nil { + ascAPI.addMessage("内购商品的销售国家/地区:\(customerTerritory) ,更新成功!✅ ") + } else { + ascAPI.addMessage("内购商品的销售国家/地区:\(customerTerritory) ,更新失败!❌ ") + } + } + + + // MARK: - 上传订阅商品 + + /// 订阅商品创建或更新 + func createRenewSubscription(appId: String, product: IAPProduct, ascAPI: APASCAPI) async { + let groupName = product.groupName + var currentSubGroup: ASCSubscriptionGroup? + // 1、是否有订阅组,没有时要先创建 + var subGroups = await ascAPI.fetchSubscriptionGroups(appId: appId) +// if subGroups.isEmpty { +// } + + for subGroup in subGroups { + if subGroup.attributes?.referenceName == groupName { + currentSubGroup = subGroup + } + } + + // 创建订阅组 + if currentSubGroup == nil, let group = await ascAPI.createSubscriptionGroups(appId: appId, groupName: groupName) { + currentSubGroup = group + subGroups.append(group) + + for localization in product.localizations { + let _ = await ascAPI.createSubscriptionGroupLocalizations(iapGroupId: group.id, name: localization.name, locale: localization.locale, customAppName: nil) + } + } + + //订阅组设置国际化 + if let group = currentSubGroup { + for localization in product.localizations { + let _ = await ascAPI.createSubscriptionGroupLocalizations(iapGroupId: group.id, name: localization.name, locale: localization.locale, customAppName: nil) + } + } + + // 2、有订阅组,获取所有订阅组的所有订阅商品 + var subscriptions = [ASCSubscription]() + for subGroup in subGroups { + let subs = await ascAPI.fetchSubscriptionGroupSubscriptions(iapGroupId: subGroup.id) + subscriptions.append(contentsOf: subs) + } + + + + // 3、查看是否存在订阅商品,不存在就创建,存在就更新 + let subs = subscriptions.filter({ $0.attributes?.productID == product.productId }) + if let sub = subs.first { + ascAPI.addMessage("订阅商品已经存在:\(product.productId) ,开始更新信息中...") + // 0. 审核备注如果原来有值,而新字段无值,则使用原值 + var product = product + if let note = sub.attributes?.reviewNote, product.reviewNote.isEmpty { + product.reviewNote = note + } + // 1.修改原商品信息 + guard let iap = await ascAPI.updateSubscription(iapId: sub.id, product: product) else { + // 修改失败 + ascAPI.addMessage("订阅商品已经存在:\(product.productId) ,更新信息失败!❌ ") + return + } + + // 2. 商品本地化语言 + ascAPI.addMessage("开始更新订阅商品本地化版本:\(product.productId)") + let localizations = await ascAPI.fetchSubscriptionLocalizations(iapId: iap.id) + for localization in product.localizations { + // 如果已经存在本地化语言,则更新 + if let locale = localizations.filter({ $0.attributes?.locale == localization.locale }).first { + // 更新 + ascAPI.addMessage("订阅商品已存在本地化版本:\(localization.locale),开始更新信息中...") + if (await ascAPI.updateSubscriptionLocalization(iapLocaleId: locale.id, localization: localization)) != nil { + // 本地化语言更新成功 + ascAPI.addMessage("订阅商品本地化版本:\(localization.locale) ,更新语言成功!✅ ") + } else { + // 本地化语言更新失败 + ascAPI.addMessage("订阅商品本地化版本:\(localization.locale) ,更新语言失败!❌ ") + } + } else { + // 创建 + await createSubscriptionLocalization(iapId: iap.id, localization: localization, product: product, ascAPI: ascAPI) + } + } + + // 3. 商品价格档位 + await updateSubscriptionPricePoint(iapId: iap.id, product: product, ascAPI: ascAPI) + + // 4. 商品截图 + await createSubscriptionScreenshot(iapId: iap.id, product: product, ascAPI: ascAPI) + + // 5. 销售国家或地区 + await updateSubscriptionAvailableTerritories(iapId: iap.id, product: product, ascAPI: ascAPI) + + } else { + // 1. 创建新的商品 + guard let iapGroupId = currentSubGroup?.id, let iap = await ascAPI.createSubscription(iapGroupId: iapGroupId, product: product) else { + // 创建失败 + ascAPI.addMessage("订阅商品:\(product.productId) ,创建失败!❌ ") + return + } + + // 2. 商品本地化语言 + for localization in product.localizations { + await createSubscriptionLocalization(iapId: iap.id, localization: localization, product: product, ascAPI: ascAPI) + } + + // 3. 商品价格档位 + await updateSubscriptionPricePoint(iapId: iap.id, product: product, ascAPI: ascAPI) + + // 4. 商品截图 + await createSubscriptionScreenshot(iapId: iap.id, product: product, ascAPI: ascAPI) + + // 5. 销售国家或地区 + await updateSubscriptionAvailableTerritories(iapId: iap.id, product: product, ascAPI: ascAPI) + } + + ascAPI.addMessage("订阅商品:\(product.productId),\(product.name) ,上传完成!\n") + } + + + /// 更新订阅商品的价格档位 + func updateSubscriptionPricePoint(iapId: String, product: IAPProduct, ascAPI: APASCAPI) async { + guard let schedule = product.priceSchedules else { + ascAPI.addMessage("无价格计划表:\(product.productId) ,请确认!❌ ") + return + } + + let baseTerritory = schedule.baseTerritory + let baseCustomerPrice = schedule.baseCustomerPrice.normalizePrice() + + ascAPI.addMessage("开始更新订阅商品价格点,基准国家:\(product.productId),\(baseTerritory),\(baseCustomerPrice) \n") + + let isPreservePrice = preserveCurrentPriceBtn.state.rawValue == 1 + ascAPI.addMessage("保留自动续期订阅者现有定价:\(isPreservePrice ? "是" : "否")") + + let points = await ascAPI.fetchSubscriptionPricePoints(iapId: iapId, territory: [baseTerritory]) + if let point = points.filter({ $0.attributes?.customerPrice!.normalizePrice() == baseCustomerPrice }).first { + + ascAPI.addMessage("开始更新自定价格:") + // 自定价格的国家或地区, 基准国家也算是自定价格 + var customerPriceSchedules = schedule.manualPrices + customerPriceSchedules.append(IAPPricePoint(territory: baseTerritory, customerPrice: baseCustomerPrice)) + let manualPricesTerritory: [String] = customerPriceSchedules.map({ $0.territory }) + // 设置自定价格 + for pricePoint in customerPriceSchedules { + let territory = pricePoint.territory + let customerPrice = pricePoint.customerPrice.normalizePrice() + let points = await ascAPI.fetchSubscriptionPricePoints(iapId: iapId, territory: [territory]) + if let point = points.filter({ $0.attributes?.customerPrice!.normalizePrice() == customerPrice }).first { + if (await ascAPI.updateSubscriptionPricePoint(iapId: iapId, pricePointId: point.id, preserveCurrentPrice: isPreservePrice)) != nil { + ascAPI.addMessage("自定价格的订阅商品的价格点:\(territory),\(customerPrice) ,更新价格成功!✅ ") + } else { + ascAPI.addMessage("自定价格的订阅商品的价格点:\(territory),\(customerPrice) ,更新价格失败!❌ ") + } + } else { + ascAPI.addMessage("自定价格的订阅商品价格点:\(territory),\(customerPrice) ,未找到此档位!❌ ") + } + } + + ascAPI.addMessage("开始更新全球均衡价格:") + // 剩余的所有的国家地区的订阅价格点,然后一个一个设置。API不支持全部国家一次配置 + let allPoints = await ascAPI.fetchSubscriptionPricePointsEqualizations(pointId: point.id, territory: nil) + for apoint in allPoints { + let territory = apoint.relationships?.territory?.data?.id ?? "" + // 自定价格的国家跳过 + if manualPricesTerritory.contains(territory) { + continue + } + let customerPrice = apoint.attributes?.customerPrice ?? "" + if (await ascAPI.updateSubscriptionPricePoint(iapId: iapId, pricePointId: apoint.id, preserveCurrentPrice: isPreservePrice)) != nil { + // 价格档位配置成功 + ascAPI.addMessage("全球均衡价格的订阅商品的价格点:\(territory),\(customerPrice) ,更新价格成功!✅ ") + } else { + // 价格档位配置失败 + ascAPI.addMessage("全球均衡价格的订阅商品的价格点:\(territory),\(customerPrice) ,更新价格失败!❌ ") + } + } + } else { + // 找不到价格档位 + ascAPI.addMessage("基准国家的订阅商品价格点:\(baseTerritory),\(baseCustomerPrice) ,未找到此档位!❌ ") + } + } + + + /// 更新订阅商品的本地化信息 + func createSubscriptionLocalization(iapId: String, localization: IAPLocalization, product: IAPProduct, ascAPI: APASCAPI) async { + ascAPI.addMessage("开始更新订阅商品本地化版本:\(product.productId),\(localization.locale)") + if (await ascAPI.createSubscriptionLocalization(iapId: iapId, localization: localization)) != nil { + // 本地化语言配置成功 + ascAPI.addMessage("订阅商品本地化版本:\(localization.locale) ,更新语言成功!✅ ") + } else { + // 本地化语言配置失败 + ascAPI.addMessage("订阅商品本地化版本:\(localization.locale) ,更新语言失败!❌ ") + } + } + + /// 更新订阅商品的送审截图 + func createSubscriptionScreenshot(iapId: String, product: IAPProduct, ascAPI: APASCAPI) async { + ascAPI.addMessage("开始更新订阅商品的送审截图:\(product.productId),\(product.reviewScreenshot)") + let imgName = product.reviewScreenshot + guard let imgPath = screenshotPaths[imgName] else { + ascAPI.addMessage("订阅商品:\(product.productId) 无送审截图或未上传截图~") + return + } + + let imaUrl = URL.init(fileURLWithPath: imgPath) + guard let fileMD5 = URL.init(fileURLWithPath: imgPath).fileMD5() else { + ascAPI.addMessage("订阅商品截图文件错误:\(imgPath) ,无法生成 md5 值~") + return + } + + let oldShot = await ascAPI.fetchSubscriptionScreenshot(iapId: iapId) + // 存在需要删除,避免文件名不一样或者过期文件 + if let ost = oldShot { + ascAPI.addMessage("删除旧的送审截图:\(ost.attributes?.fileName ?? "")") + let status = await ascAPI.deleteSubscriptionScreenshot(iapShotId: ost.id) + if status != 204 { + ascAPI.addMessage("订阅商品截图创建失败:\(imgName) ,无法删除旧截图~") + } + } + + ascAPI.addMessage("创建新的送审截图:\(product.reviewScreenshot)") + // 创建截图 + let imaSize = imaUrl.fileSizeInt() + guard let shot = await ascAPI.createSubscriptionScreenshot(iapId: iapId, fileName: imgName, fileSize: imaSize) else { + // 创建失败 + ascAPI.addMessage("订阅商品:\(product.productId) ,创建送审截图失败!❌ ") + return + } + + // 根据苹果接口返回的上传接口上传 + guard let method = shot.attributes?.uploadOperations?.first?.method, + let url = shot.attributes?.uploadOperations?.first?.url, + let requestHeaders = shot.attributes?.uploadOperations?.first?.requestHeaders, + let baseURL = URL(string: url) else { + ascAPI.addMessage("订阅商品:\(product.productId) ,创建送审截图失败!苹果参数异常~ ❌ ") + return + } + + var request = URLRequest(url: baseURL) + request.httpMethod = method + for header in requestHeaders { + request.headers[header.name ?? ""] = header.value ?? "" + } + + ascAPI.addMessage("上传新的送审截图:\(product.reviewScreenshot)") + // 上传图片 + guard let response = try? await URLSession.shared.upload(for: request, fromFile: imaUrl) else { + ascAPI.addMessage("订阅商品:\(product.productId) ,创建送审截图失败!上传图片异常~ ❌ ") + return + } + guard let responseCode = (response.1 as? HTTPURLResponse)?.statusCode, responseCode == 200 else { + ascAPI.addMessage("订阅商品:\(product.productId) ,创建送审截图失败!上传图片异常 \(response.1.description)~ ❌ ") + return + } + + ascAPI.addMessage("提交新的送审截图:\(product.reviewScreenshot)") + // 确认图片 + if ((await ascAPI.updateSubscriptionScreenshot(iapShotId: shot.id, fileMD5: fileMD5)) != nil) { + ascAPI.addMessage("订阅商品:\(product.productId) ,送审截图上传成功!✅ ") + } else { + ascAPI.addMessage("订阅商品:\(product.productId) ,送审截图可能上传失败! ") + } + } + + /// 销售国家或地区 + func updateSubscriptionAvailableTerritories(iapId: String, product: IAPProduct, ascAPI: APASCAPI) async { + let inAll = product.territories.availableInAllTerritories + let inNew = product.territories.availableInNewTerritories + let summary = territoryInfo(product: product) + let newTerritory = inNew ? "将来新国家(地区)时自动提供!" : "将来新国家(地区)时不自动提供!" + ascAPI.addMessage("开始更新订阅商品的销售国家/地区:\(summary)") + + guard !inAll else { + var allTerritories: [[String: String]] = [] + if let territories = await ascAPI.territories() { + territories.forEach { territory in + allTerritories.append([ + "type": "territories", + "id": territory.id + ]) + } + // 更新全部国家地区 + if (await ascAPI.updateSubscriptionAvailabilityTerritories(iapId: iapId, availableTerritories: allTerritories, availableInNewTerritories: inNew)) != nil { + ascAPI.addMessage("选择:所有国家(地区)销售,\(newTerritory),更新成功!✅ ") + } else { + ascAPI.addMessage("选择:所有国家(地区)销售,\(newTerritory),更新失败!❌ ") + } + } else { + ascAPI.addMessage("选择:所有国家(地区)销售,\(newTerritory),无法设置!获取国家标识码失败!❌ ") + } + return + } + + /// 选择销售的国家或地区 + var territories: [[String: String]] = [] + product.territories.territories?.forEach({ territory in + territories.append([ + "type": "territories", + "id": territory.id + ]) + }) + + let customerTerritory = product.territories.territories?.map({ $0.id }).joined(separator: ",") ?? "无" + if (await ascAPI.updateSubscriptionAvailabilityTerritories(iapId: iapId, availableTerritories: territories, availableInNewTerritories: inNew)) != nil { + ascAPI.addMessage("订阅商品的销售国家/地区:\(customerTerritory) ,更新成功!✅ ") + } else { + ascAPI.addMessage("订阅商品的销售国家/地区:\(customerTerritory) ,更新失败!❌ ") + } + } +} + +// MARK: - Privacy Method +extension APUploadIAPListVC { + + func territoryInfo(product: IAPProduct) -> String { + let inAll = product.territories.availableInAllTerritories + let inNew = product.territories.availableInNewTerritories + let customerTerritory = product.territories.territories?.map({ $0.id }).joined(separator: ",") ?? "" + let off = !inAll && !inNew && (product.territories.territories?.isEmpty ?? true) + let territory = off ? "下架" : (customerTerritory.isEmpty ? (inAll ? "全部" : "当前下架") : customerTerritory) + let stringValue = "在所有国家/地区销售:'\(inAll ? "是" : "否")'\n将来新国家/地区自动提供:'\(inNew ? "是" : "否")'\n指定国家/地区销售:\(territory)" + return stringValue + } +} + + +// MARK: - NSTableViewDelegate +extension APUploadIAPListVC: NSTableViewDelegate, NSTableViewDataSource { + func numberOfRows(in tableView: NSTableView) -> Int { + return iaps.count + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + let iap = iaps[row] + switch tableColumn?.identifier.enumValue() { + case ColumnIdetifier.id.cellValue: + let cell = tableView.makeView(withIdentifier: ColumnIdetifier.id.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = String(row+1) + return cell + case ColumnIdetifier.productID.cellValue: + let cell = tableView.makeView(withIdentifier: ColumnIdetifier.productID.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = iap.productId + return cell + case ColumnIdetifier.productName.cellValue: + let cell = tableView.makeView(withIdentifier: ColumnIdetifier.productName.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = iap.name + return cell + case ColumnIdetifier.price.cellValue: + let cell = tableView.makeView(withIdentifier: ColumnIdetifier.price.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = territoryInfo(product: iap) + return cell + case ColumnIdetifier.level.cellValue: + let cell = tableView.makeView(withIdentifier: ColumnIdetifier.level.cellValue, owner: self) as? NSTableCellView + let territory = iap.priceSchedules?.baseTerritory ?? "-" + let price = iap.priceSchedules?.baseCustomerPrice ?? "-" + let customerPrice = iap.priceSchedules?.manualPrices.map({ pp in + "{'国家:'\(pp.territory)', '自定价格':'\(pp.customerPrice)'}\n" + }).joined() ?? "-" + cell?.textField?.stringValue = "基准国家:'\(territory)'\n基准价格:'\(price)'\n\(customerPrice)" + return cell + case ColumnIdetifier.productPds.cellValue: + let cell = tableView.makeView(withIdentifier: ColumnIdetifier.productPds.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = iap.reviewNote + return cell + case ColumnIdetifier.state.cellValue: + let cell = tableView.makeView(withIdentifier: ColumnIdetifier.state.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = iap.inAppPurchaseType.CNValue() + return cell + case ColumnIdetifier.screenshot.cellValue: + let cell = tableView.makeView(withIdentifier: ColumnIdetifier.screenshot.cellValue, owner: self) as? ImageViewCell + let file_name = iap.reviewScreenshot + let imgPath = screenshotPaths[file_name] ?? "" + cell?.imgSel.image = NSImage(contentsOfFile: imgPath) + return cell + case ColumnIdetifier.language.cellValue: + let cell = tableView.makeView(withIdentifier: ColumnIdetifier.language.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = iap.localizations.map({ lz in + "{'locale:'\(lz.locale)', 'title':'\(lz.name)', 'desc':'\(lz.description)'}\n" + }).joined() + return cell + default: + return nil + } + } + + func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { + let prices = iaps[row].priceSchedules?.manualPrices.count ?? 0 + let count = max(prices + 2, 3) + return count > 10 ? CGFloat(20 * count) : CGFloat(25 * count) + } +} diff --git a/AppleParty/AppleParty/AppListView/InAppPurchseView/DragView.swift b/AppleParty/AppleParty/AppListView/InAppPurchseView/DragView.swift new file mode 100644 index 0000000..b58ba64 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/InAppPurchseView/DragView.swift @@ -0,0 +1,64 @@ +// +// DragView.swift +// AppleParty +// +// Created by 易承 on 2020/12/16. +// + +import Cocoa + +protocol DragViewDelegate { + func dragView(_ path: String?) +} + +class DragView: NSView { + + var delegate: DragViewDelegate? + + private var fileTypeIsOk = false + let NSFilenamesPboardType = NSPasteboard.PasteboardType("NSFilenamesPboardType") + let fileTypes = ["jpg", "jpeg", "png"] + var droppedFilePath: String? + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + // Declare and register an array of accepted types + registerForDraggedTypes([NSPasteboard.PasteboardType(kUTTypeFileURL as String), + NSPasteboard.PasteboardType(kUTTypeItem as String)]) + } + + override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { + fileTypeIsOk = checkExtension(drag: sender) + return [] + } + + override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation { + return fileTypeIsOk ? .link : [] + } + + override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { + if let board = sender.draggingPasteboard.propertyList(forType: NSFilenamesPboardType) as? NSArray, let imagePath = board[0] as? String { + // THIS IS WERE YOU GET THE PATH FOR THE DROPPED FILE + droppedFilePath = imagePath + if fileTypeIsOk { + delegate?.dragView(droppedFilePath) + } + return true + } + return false + } + + fileprivate func checkExtension(drag: NSDraggingInfo) -> Bool { + if let board = drag.draggingPasteboard.propertyList(forType: NSFilenamesPboardType) as? NSArray, let path = board[0] as? String { + let url = NSURL(fileURLWithPath: path) + if let fileExtension = url.pathExtension?.lowercased() { + return fileTypes.contains(fileExtension) + } + } + return false + } +} diff --git a/AppleParty/AppleParty/AppListView/InAppPurchseView/IAPUploadImageVC.swift b/AppleParty/AppleParty/AppListView/InAppPurchseView/IAPUploadImageVC.swift new file mode 100644 index 0000000..c16b273 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/InAppPurchseView/IAPUploadImageVC.swift @@ -0,0 +1,105 @@ +// +// UploadVC.swift +// AppleParty +// +// Created by 易承 on 2021/6/3. +// + +import AppKit +import Foundation + +class IAPUploadImageVC: NSViewController { + + @IBOutlet weak var tableView: NSTableView! + + @IBOutlet weak var cancelBtm: NSButton! + @IBOutlet weak var submitBtm: NSButton! + @IBOutlet weak var tipLb: NSTextField! + var picnames = [String]() + var resultPaths = [String: String]() + + typealias CallBackFunc = (_ paths: [String: String]) -> Void + var callBackFunc: CallBackFunc? + + fileprivate lazy var fileTypes: [String] = { + return ["jpg", "jpeg", "png"] + }() + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.delegate = self + tableView.dataSource = self + tipLb.stringValue = "需要上传\(picnames.count)张图片" + } + + @IBAction func clickedBatchUploadBtn(_ sender: Any) { + let openPanel = NSOpenPanel() + openPanel.canChooseFiles = true + openPanel.canChooseDirectories = false + openPanel.allowsMultipleSelection = true + openPanel.allowedFileTypes = fileTypes + openPanel.beginSheetModal(for: self.view.window!) { (modalResponse: NSApplication.ModalResponse) in + if modalResponse == .OK { + openPanel.urls.forEach { url in + let picname = url.lastPathComponent + debugPrint(picname) + if self.picnames.contains(picname) { + debugPrint("contains") + self.resultPaths[picname] = url.path + } + } + self.tableView.reloadData() + } + } + } + + + @IBAction func cancel(_ sender: Any) { + dismiss(self) + } + + @IBAction func submit(_ sender: Any) { + guard picnames.count == resultPaths.keys.count else { + APHUD.hide(message: "必须图片数量不正确!", view: self.view) + return + } + dismiss(self) + if let callBackFunc = callBackFunc { + callBackFunc(resultPaths) + } + } +} + +extension IAPUploadImageVC: NSTableViewDelegate, NSTableViewDataSource { + func numberOfRows(in tableView: NSTableView) -> Int { + return picnames.count + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + switch tableColumn?.identifier.enumValue() { + case ColumnIdetifier.picname.cellValue: + let cell = tableView.makeView(withIdentifier: ColumnIdetifier.picname.cellValue, owner: self) as? NSTableCellView + cell?.textField?.stringValue = picnames[row] + return cell + case ColumnIdetifier.upload.cellValue: + let cell = tableView.makeView(withIdentifier: ColumnIdetifier.upload.cellValue, owner: self) as? UploadCell + cell?.row = row + cell?.dragView(resultPaths[picnames[row]]) + cell?.callBackFunc = { path,crow in + self.resultPaths[self.picnames[crow]] = path + } + return cell + default: + return nil + } + } + + func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { + 100 + } + + func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { + return false + } +} diff --git a/AppleParty/AppleParty/AppListView/InAppPurchseView/InAppPurchseView.storyboard b/AppleParty/AppleParty/AppListView/InAppPurchseView/InAppPurchseView.storyboard new file mode 100644 index 0000000..b852f64 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/InAppPurchseView/InAppPurchseView.storyboard @@ -0,0 +1,993 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/AppListView/InAppPurchseView/Models/IAPExcelParser.swift b/AppleParty/AppleParty/AppListView/InAppPurchseView/Models/IAPExcelParser.swift new file mode 100644 index 0000000..13d07f6 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/InAppPurchseView/Models/IAPExcelParser.swift @@ -0,0 +1,421 @@ +// +// IAPExcelParser.swift +// AppleParty +// +// Created by HTC on 2022/11/10. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Foundation +import CoreXLSX + + +/// 上传的商品类型 +enum IAPType: String { + case CONSUMABLE = "CONSUMABLE" + case NON_CONSUMABLE = "NON_CONSUMABLE" + case NON_RENEWING_SUBSCRIPTION = "NON_RENEWING_SUBSCRIPTION" + case AUTO_RENEWABLE = "auto-renewable" + case UNKNOW = "unknown" + + static func type(name: String) -> IAPType { + switch name { + case "消耗型": + return .CONSUMABLE + case "非消耗型": + return .NON_CONSUMABLE + case "非续期订阅": + return .NON_RENEWING_SUBSCRIPTION + case "自动续期订阅": + return .AUTO_RENEWABLE + default: + return .UNKNOW + } + } + + func CNValue() -> String { + switch self { + case .CONSUMABLE: + return "消耗型" + case .NON_CONSUMABLE: + return "非消耗型" + case .NON_RENEWING_SUBSCRIPTION: + return "非续期订阅" + case .AUTO_RENEWABLE: + return "自动续期订阅" + default: + return "未知" + } + } +} + +// 本地化名称和描述 +struct IAPLocalization { + var name: String = "" + var description: String = "" + var locale: String = "" +} + +// 订阅类型的子字段 +struct IAPSubscriptions { + var groupLevel: Int = 1 + var subscriptionPeriod: String = "ONE_MONTH" //ONE_WEEK, ONE_MONTH, TWO_MONTHS, THREE_MONTHS, SIX_MONTHS, ONE_YEAR +} + +/// 内购统一模型 +struct IAPProduct { + var name: String = "" + var groupName: String = "" + var productId: String = "" + var reviewNote: String = "" + var reviewScreenshot: String = "" + var familySharable: Bool = false + var inAppPurchaseType: IAPType = .UNKNOW //# CONSUMABLE、NON_CONSUMABLE、NON_RENEWING_SUBSCRIPTION + var localizations: [IAPLocalization] = [] + // 订阅类型的特有 + var subscriptions: IAPSubscriptions? + // 价格计划表 + var priceSchedules: IAPPriceSchedules? + // 销售的国家或地区 + var territories: IAPTerritories = IAPTerritories() +} + +/// 价格计划表 +struct IAPPriceSchedules { + var productId: String = "" + var baseTerritory: String = "" + var baseCustomerPrice: String = "" + var manualPrices: [IAPPricePoint] = [] +} + +/// 价格点 +struct IAPPricePoint { + var territory: String + var customerPrice: String +} + +/// 销售的国家或地区 +struct IAPTerritories { + var productId: String = "" + /// 所有国家或地区销售(包括将来新国家或地区) + var availableInAllTerritories: Bool = true + /// 将来新国家/地区自动提供销售 + var availableInNewTerritories: Bool = true + var territories: [IAPTerritory]? +} + +struct IAPTerritory { + var id: String +} + + +struct IAPExcelParser { + + static func parser(_ filePath: URL) -> [IAPProduct] { + guard let file = XLSXFile(filepath: filePath.path) else { + fatalError("XLSX file at \(filePath.path) is corrupted or does not exist") + } + + // 先读取价格计划表 + let priceSchedules = parserPricePoints(file) + // 销售的国家或地区 + let territories = parserTerritories(file) + + var result: [IAPProduct] = [] + for wbk in try! file.parseWorkbooks() { + for (name, path) in try! file.parseWorksheetPathsAndNames(workbook: wbk) { + if let worksheetName = name { + print("This worksheet has a name: \(worksheetName)") + } + + if name != "AppleParty" { + continue + } + + guard let worksheet = try? file.parseWorksheet(at: path), + let sharedStrings = try! file.parseSharedStrings() else { + print("This worksheet/sharedStrings is null") + return result + } + var index = 0 + var columnTitles = [String]() + var columnIndexs = [String]() + for row in worksheet.data?.rows ?? [] { + var columnValues = [String: String]() + index += 1 + for cell in row.cells { + let key = cell.reference.column.value + var columnStrings = cell.stringValue(sharedStrings) ?? "" + // 富文本读取 + var richStr = "" + let richColumnCString = cell.richStringValue(sharedStrings) + for richChar in richColumnCString { + richStr += string(from: richChar.text) + } + columnStrings += richStr + // 第一行作为标识行,用于多语言标识,默认信任此字段 + if index == 1 { + columnTitles.append(columnStrings) + columnIndexs.append(key) + } else { + columnValues[key] = columnStrings + } + } + + if index == 1 { + print(columnTitles) + print(columnIndexs) + continue + } + + // Product ID 参考名字 内购买类型 审核截图(可选) 审核备注(可选) zh-Hans zh-Hans ja ja ko ko + var iap = IAPProduct() + iap.groupName = columnValues["A"] ?? "" + iap.productId = columnValues["B"] ?? "" + iap.name = columnValues["C"] ?? "" + let productType = columnValues["D"] ?? "" + iap.inAppPurchaseType = IAPType.type(name: productType) + + // 订阅类型默认的字段 + if iap.inAppPurchaseType == .AUTO_RENEWABLE { + let period = columnValues["E"] ?? "" + iap.subscriptions = IAPSubscriptions() + iap.subscriptions?.subscriptionPeriod = period + } + + iap.reviewScreenshot = columnValues["F"] ?? "" + iap.reviewNote = columnValues["G"] ?? "" + + // 非法的行 + if iap.productId.isEmpty, iap.name.isEmpty { + continue + } + + // 【产品 ID】 可以由字母、数字、下划线(_)和句点(.)构成。 2 ~ 100 个字符) + if iap.productId.count < 2 || iap.productId.count > 100 { + NSAlert.show("Product ID 长度为:2~100 字符!") + } + // 参考名字 + if iap.name.count < 2 || iap.name.count > 64 { + NSAlert.show("\(iap.productId):“参考名字”长度超过 2~64 字符!") + } + + + + // 价格计划表 + let schedules = priceSchedules.filter({ $0.productId == iap.productId }) + if let schedule = schedules.first { + iap.priceSchedules = schedule + } + + //销售国家和地区 + let territorys = territories.filter({ $0.productId == iap.productId }) + if let territory = territorys.first { + iap.territories = territory + } + + // 商品本地化名称和描述 + var localizations: [IAPLocalization] = [] + // 本地化的标识,从下标7开始,奇数遍历,成对出现的 + let columeMax = columnIndexs.count + let columeEndIndex = columnIndexs.count - 1 + for idx in stride(from: 7, to: columeEndIndex, by: 2){ + if idx + 1 <= columeMax { + let locale = columnTitles[idx] + let key1 = columnIndexs[idx] + let key2 = columnIndexs[idx+1] + let name = columnValues[key1] ?? "" + let description = columnValues[key2] ?? "" + if !name.isEmpty && !description.isEmpty { + var localization = IAPLocalization() + localization.locale = locale + localization.name = name + localization.description = description + localizations.append(localization) + } + } + } + iap.localizations = localizations + result.append(iap) + } + } + } + return result + } + + /// 公共方法 + fileprivate static func handleRowContents(_ row: Row, _ sharedStrings: SharedStrings, _ index: Int, _ columnIndexs: inout [String], _ columnValues: inout [String : String]) { + for cell in row.cells { + let key = cell.reference.column.value + var columnStrings = cell.stringValue(sharedStrings) ?? "" + // 富文本读取 + var richStr = "" + let richColumnCString = cell.richStringValue(sharedStrings) + for richChar in richColumnCString { + richStr += string(from: richChar.text) + } + columnStrings += richStr + // 第一行作为标识行,用于多语言标识,默认信任此字段 + if index == 1 { + columnIndexs.append(key) + } else { + columnValues[key] = columnStrings + } + } + } + + /// 读取价格计划表 + static func parserPricePoints(_ file: XLSXFile) -> [IAPPriceSchedules] { + + var result: [IAPPriceSchedules] = [] + for wbk in try! file.parseWorkbooks() { + for (name, path) in try! file.parseWorksheetPathsAndNames(workbook: wbk) { + if let worksheetName = name { + print("This worksheet has a name: \(worksheetName)") + } + + if name != "PricePoints" { + continue + } + + guard let worksheet = try? file.parseWorksheet(at: path), + let sharedStrings = try! file.parseSharedStrings() else { + print("This worksheet/sharedStrings is null") + return result + } + var index = 0 + var columnIndexs = [String]() + for row in worksheet.data?.rows ?? [] { + var columnValues = [String: String]() + index += 1 + handleRowContents(row, sharedStrings, index, &columnIndexs, &columnValues) + + // 第一行是标题行,忽视 + if index == 1 { + print(columnIndexs) + continue + } + + // Product ID 基准国家(代码) 基准国价格 自定价格国家1 自定价格1 自定价格国家2 自定价格2 + let productId = columnValues["A"] ?? "" + let baseTerritory = columnValues["B"] ?? "" + let baseCustomerPrice = (columnValues["C"] ?? "").twoDecimalPrice() + + // 非法的行 + if productId.isEmpty, baseTerritory.isEmpty, baseCustomerPrice.isEmpty { + continue + } + + // 【产品 ID】 可以由字母、数字、下划线(_)和句点(.)构成。 2 ~ 100 个字符) + if productId.count < 2 || productId.count > 100 { + NSAlert.show("PricePoints Product ID 长度为:2~100 字符!") + } + + // 价格计划表 + var schedule = IAPPriceSchedules(productId: productId, baseTerritory: baseTerritory, baseCustomerPrice: baseCustomerPrice) + + // 自定价格的国家和价格 + var manualPrices: [IAPPricePoint] = [] + // 自定价格,从下标3开始,奇数遍历,成对出现的 + let columeMax = columnValues.count + let columeEndIndex = columnValues.count - 1 + for idx in stride(from: 3, to: columeEndIndex, by: 2) { + if idx + 1 <= columeMax, columnIndexs.count > idx+1 { + let key1 = columnIndexs[idx] + let key2 = columnIndexs[idx+1] + let name = columnValues[key1] ?? "" + let price = (columnValues[key2] ?? "").twoDecimalPrice() + if !name.isEmpty && !price.isEmpty { + let pricePoint = IAPPricePoint(territory: name, customerPrice: price) + manualPrices.append(pricePoint) + } + } + } + schedule.manualPrices = manualPrices + result.append(schedule) + } + } + } + return result + } + + + /// 读取销售的国家或地区 + static func parserTerritories(_ file: XLSXFile) -> [IAPTerritories] { + + var result: [IAPTerritories] = [] + for wbk in try! file.parseWorkbooks() { + for (name, path) in try! file.parseWorksheetPathsAndNames(workbook: wbk) { + if let worksheetName = name { + print("This worksheet has a name: \(worksheetName)") + } + + if name != "Territories" { + continue + } + + guard let worksheet = try? file.parseWorksheet(at: path), + let sharedStrings = try! file.parseSharedStrings() else { + print("This worksheet/sharedStrings is null") + return result + } + var index = 0 + var columnIndexs = [String]() + for row in worksheet.data?.rows ?? [] { + var columnValues = [String: String]() + index += 1 + handleRowContents(row, sharedStrings, index, &columnIndexs, &columnValues) + + // 第一行是标题行,忽视 + if index == 1 { + print(columnIndexs) + continue + } + + // Product ID 在所有国家/地区销售(1是,0否) 将来新国家/地区自动提供(1是,0否) 销售1 销售2 ... + let productId = columnValues["A"] ?? "" + let availableInAllTerritories = columnValues["B"] ?? "" + let availableInNewTerritories = columnValues["C"] ?? "" + let isInAll = availableInAllTerritories == "1" ? true : false + let isInNew = availableInNewTerritories == "1" ? true : false + + // 非法的行 + if productId.isEmpty { + continue + } + + // 【产品 ID】 可以由字母、数字、下划线(_)和句点(.)构成。 2 ~ 100 个字符) + if productId.count < 2 || productId.count > 100 { + NSAlert.show("Territories Product ID 长度为:2~100 字符!") + } + + // 销售的国家或地区 + var territory = IAPTerritories(productId: productId, availableInAllTerritories: isInAll, availableInNewTerritories: isInNew) + + // 如果在所有国家或地区销售,则不在读取自定销售国家或地区 + if isInAll { + territory.availableInNewTerritories = true + result.append(territory) + continue + } + + // 自定销售的国家或地区 + var territories: [IAPTerritory] = [] + // 从下标3开始 + let columeMax = columnValues.count + for idx in stride(from: 3, to: columeMax, by: 1) { + let key = columnIndexs[idx] + let name = columnValues[key] ?? "" + if !name.isEmpty { + let pricePoint = IAPTerritory(id: name) + territories.append(pricePoint) + } + } + territory.territories = territories + result.append(territory) + } + } + } + return result + } +} diff --git a/AppleParty/AppleParty/AppListView/InAppPurchseView/Models/IAPModel.swift b/AppleParty/AppleParty/AppListView/InAppPurchseView/Models/IAPModel.swift new file mode 100644 index 0000000..8d9ea64 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/InAppPurchseView/Models/IAPModel.swift @@ -0,0 +1,341 @@ +// +// ITCResponseModel.swift +// AppleParty +// +// Created by 易承 on 2021/5/13. +// +// HTTP请求返回的数据解析类 + +import Foundation +import Cocoa + + +/* 消耗型项目consumable + 非消耗型non-consumable + 自动续期订阅auto-renewable + 非续期订阅subscription + */ +// 上传的商品类型 +enum InAppPurchaseType: String { + case CONSUMABLE = "consumable" + case NON_CONSUMABLE = "non-consumable" + case AUTO_RENEWABLE = "auto-renewable" + case FREE_SUBSCRIPTION = "free-subscription" + case SUBSCRIPTION = "subscription" + case UNKNOW = "unknown" + + func CNValue() -> String { + switch self { + case .CONSUMABLE: + return "消耗型" + case .NON_CONSUMABLE: + return "非消耗型" + case .SUBSCRIPTION: + return "非续订型" + case .AUTO_RENEWABLE: + return "自动续订型" + case .FREE_SUBSCRIPTION: + return "免费订阅型" + default: + return "未知" + } + } +} + + +/* + "ITC.addons.type.consumable": "消耗型项目", + "ITC.addons.type.freeSubscription": "免费订阅", + "ITC.addons.type.nonConsumable": "非消耗型项目", + "ITC.addons.type.recurring": "自动续期订阅", + "ITC.addons.type.subscription": "非续期订阅", + */ +// 苹果后台商品类型 +enum ITCAddOnType: String { + case consumable = "ITC.addons.type.consumable" + case subscription = "ITC.addons.type.subscription" + case free_subscription = "ITC.addons.type.freeSubscription" + case non_consumable = "ITC.addons.type.nonConsumable" + case auto_renewable = "ITC.addons.type.recurring" + case unknown + + func CNValue() -> String { + switch self { + case .consumable: + return "消耗型项目" + case .non_consumable: + return "非消耗型项目" + case .subscription: + return "非续期订阅" + case .auto_renewable: + return "自动续期订阅" + case .free_subscription: + return "免费订阅" + default: + return "未知" + } + } +} + + +/* + "approved": "已批准", + "created": "已创建", + "deleted": "已删除", + "deletePending": "正在删除", + "developerActionNeeded": "需要开发人员操作", + "developerRemovedFromSale": "被开发人员下架", + "developerSignedOff": "开发人员签名", + "inReview": "正在审核", + "missingMetadata": "元数据丢失", + "pendingBinaryApproval": "正在审核", + "pendingDeveloperRelease": "等待开发人员发布", + "pendingScreenshot": "正在等待屏幕快照", + "prepareForSubmission": "准备提交", + "processingContentUpload": "正在处理", + "readyForSale": "已批准", + "readyToSubmit": "准备提交", + "rejected": "被拒绝", + "removedFromSale": "被下架", + "replaced": "被替换", + "waitingForContentUpload": "正在等待上传", + "waitingForReview": "正在等待审核", + */ +// iap状态 +enum InAppPurchaseState: String { + case readyForSale + case missingMetadata + case developerActionNeeded + case developerRemovedFromSale + case readyToSubmit + case prepareForSubmission + case waitingForReview + case waitingForContentUpload + case inReview + case pendingBinaryApproval + case pendingDeveloperRelease + case rejected + case removedFromSale + case approved + case created + case deleted + case deletePending + case developerSignedOff + case pendingScreenshot + case processingContentUpload + case replaced + case unknown + + var statusValue: (String, NSColor) { + switch self { + case .readyForSale: + return ("可供销售", NSColor(calibratedRed: 0.23, green: 0.64, blue: 0.40, alpha: 1.00)) + case .missingMetadata: + return ("元数据丢失", NSColor(calibratedRed: 0.97, green: 0.50, blue: 0.19, alpha: 1.00)) + case .developerActionNeeded: + return ("需要开发人员操作", NSColor(calibratedRed: 0.95, green: 0.00, blue: 0.13, alpha: 1.00)) + case .developerRemovedFromSale: + return ("被开发人员下架", NSColor(calibratedRed: 0.95, green: 0.00, blue: 0.13, alpha: 1.00)) + case .readyToSubmit, .prepareForSubmission: + return ("准备提交", NSColor(calibratedRed: 0.23, green: 0.64, blue: 0.40, alpha: 1.00)) + case .waitingForReview: + return ("正在等待审核", NSColor(calibratedRed: 0.900, green:0.658, blue:0.625, alpha:1.000)) + case .waitingForContentUpload: + return ("正在等待上传", NSColor(calibratedRed: 0.23, green: 0.64, blue: 0.40, alpha: 1.00)) + case .inReview, .pendingBinaryApproval: + return ("正在审核", NSColor(calibratedRed: 0.999, green:0.775, blue:0.031, alpha:1.000)) + case .pendingDeveloperRelease: + return ("等待开发人员发布", NSColor(calibratedRed: 0.23, green: 0.64, blue: 0.40, alpha: 1.00)) + case .rejected: + return ("被拒绝", NSColor(calibratedRed:0.548, green:0.145, blue:0.781, alpha:1.000)) + case .removedFromSale: + return ("被下架", NSColor(calibratedRed:0.906, green:0.148, blue:0.155, alpha:1.000)) + default: + return (self.rawValue, NSColor.secondaryLabelColor) + } + } +} + +// MARK: - 内购列表-新 +struct Product { + var type: String = "" + var id: String = "" + var referenceName: String = "" + var productId: String = "" + var inAppPurchaseType: InAppPurchaseType = .UNKNOW + var state: InAppPurchaseState = .unknown +} + +struct ProductList { + var products: [Product] + + init(body: [String: Any]) { + products = [Product]() + let data = dictionaryArray(body["data"]) + for temp in data { + var product = Product() + product.type = string(from: temp["type"]) + product.id = string(from: temp["id"]) + let attributes = dictionary(temp["attributes"]) + product.referenceName = string(from: attributes["referenceName"]) + product.productId = string(from: attributes["productId"]) + product.inAppPurchaseType = InAppPurchaseType(rawValue: string(from: attributes["inAppPurchaseType"])) ?? .UNKNOW + product.state = InAppPurchaseState(rawValue: string(from: attributes["state"])) ?? .unknown + products.append(product) + } + } +} + +// MARK: - 内购列表-旧 +struct IAPList { + struct IAP { + struct Version { + var screenshotUrl: String = "" + var itunesConnectStatus: String = "" + var issuesCount: Int = 0 + var canSubmit: Bool = false + + init(dict: [String: Any]) { + screenshotUrl = string(from: dict["screenshotUrl"]) + itunesConnectStatus = string(from: dict["itunesConnectStatus"]) + issuesCount = int(from: dict["issuesCount"]) ?? 0 + canSubmit = bool(from: dict["canSubmit"]) + } + } + + var familyReferenceName: String = "" + var durationDays: Int = 0 + var numberOfCodes: Int = 0 + var maximumNumberOfCodes: Int = 0 + var appMaximumNumberOfCodes: Int = 0 + var isEditable: Bool = false + var isRequired: Bool = false + var canDeleteAddOn: Bool = false + var errorKeys: String = "" + var itcsubmitNextVersion: Bool = false + var isEmptyValue: Bool = false + var adamId: String = "" // appleid + var referenceName: String = "" // 商品名称 + var vendorId: String = "" // 商品id + var addOnType: ITCAddOnType = .unknown // 商品类型 + var versions: [Version] = [] + var purpleSoftwareAdamIds: [String] = [] + var lastModifiedDate: String = "" + var isNewsSubscription: Bool = false + var iTunesConnectStatus: InAppPurchaseState = .unknown + + // detail + var status: String = "" // 状态 + var familySharable: Bool = false // 家庭共享 + var availableInAllTerritories: Bool = true // 可供销售 + var reviewNote: String = "" // 截图 + var reviewScreenshot: String = "" // 截图 + var localizations: [IAPLocalization] = [] // 本地化描述 + + mutating func updateDetail(body: [String: Any]) { + let data = dictionary(body["data"]) + status = string(from: dictionary(dictionary(data)["attributes"])["state"]) + reviewNote = string(from: dictionary(dictionary(data)["attributes"])["reviewNote"]) + familySharable = bool(from: dictionary(dictionary(data)["attributes"])["familySharable"]) + + let included = dictionaryArray(body["included"]) + for include in included { + guard let type = include["type"] as? String else { + return + } + + if type == "inAppPurchaseLocalizations" { + let attr = dictionary(include["attributes"]) + let name = string(from: attr["name"]) + let locale = string(from: attr["locale"]) + let description = string(from: attr["description"]) + localizations.append(IAPLocalization(name: name, description: description, locale: locale)) + } + + if type == "inAppPurchaseAppStoreReviewScreenshots" { + let attr = dictionary(include["attributes"]) + let asset = dictionary(attr["imageAsset"]) + let width = string(from: asset["width"]) + let height = string(from: asset["height"]) + let templateUrl = string(from: asset["templateUrl"]) + // {w}x{h}bb.{f} + let reviewScreenshot = templateUrl + .replacingOccurrences(of: "{w}", with: width) + .replacingOccurrences(of: "{h}", with: height) + .replacingOccurrences(of: "{f}", with: "png") + self.reviewScreenshot = reviewScreenshot + } + } + } + + // price + var priceTier: String = "" // 价格等级 + + mutating func updatePrices(body: [String: Any]) { + let included = dictionaryArray(body["included"]) + for include in included { + let attr = dictionary(include["attributes"]) + self.priceTier = string(from: attr["priceTier"]) + } + } + + /// 本地属性 + var isSelected = false // 是否标记为批量选中 + var curid = 0 // 数组中的序号 + + var app: App + init(app: App) { + self.app = app + } + } + + var iapList: [IAP] + var app: App + + init(body: [String: Any], app: App) { + iapList = [IAP]() + let data = dictionaryArray(body["data"]) + for i in 0.. Bool in + let nonDigits = CharacterSet.decimalDigits.inverted + let numStr1 = iap1.vendorId.trimmingCharacters(in: nonDigits) + let numStr2 = iap2.vendorId.trimmingCharacters(in: nonDigits) + return int(from: numStr1) ?? 0 < int(from: numStr2) ?? 0 + }) + self.app = app + } +} + diff --git a/AppleParty/AppleParty/AppListView/InAppPurchseView/Models/XMLModel.swift b/AppleParty/AppleParty/AppListView/InAppPurchseView/Models/XMLModel.swift new file mode 100644 index 0000000..649e254 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/InAppPurchseView/Models/XMLModel.swift @@ -0,0 +1,364 @@ +// +// XMLModel.swift +// AppleParty +// +// Created by 易承 on 2021/5/26. +// + +import Foundation +import SWXMLHash + + +struct In_App_Purchase { + + var product_id = "" // 商品id + var reference_name = "" // 商品名称 + var type: InAppPurchaseType = .UNKNOW // 商品类型 + var wholesale_price_tier = 0 // 价格等级 + var title = "" // 本地化title + var description = "" // 本地化描述 + var file_name = "" // 截图文件名 + var size = "" // 截图大小 + var checksum = "" // 截图md5 + var review_notes = "" // 商品描述 + var lang = "zh-Hans" // 本地化语言 + + var inputPrice = "" // 表格的价格 +} + +struct Screen_Shot { + var file_name = "" // 文件名 + var size = "" // 大小 + var checksum = "" // md5 + var position = "0" // 位置 + var preview_time = "00:00:05:00" //设置默认视频时间节点 +} + +struct XMLModel { + var provider = UserCenter.shared.developerTeamId // 公司名称 + var team_id = UserCenter.shared.developerTeamId // 开发者teamid + var vendor_id = "" // sku 套装id + + // 内购品项 + var iaps: [In_App_Purchase] = [] + + // 商店截图 + var app_locale = "zh-Hans" // 本地化语言 + var app_platform = "ios" // ios 或 osx + var app_version = "" //当前版本 + var app_title = "" //商店应用名 + var shots: [String: [Screen_Shot]] = [:] + var videos: [String: [Screen_Shot]] = [:] + + // 上传ipa文件 + var apple_id = "" // apple id + var archive_type = "bundle" //上传文件类型 + var ipa_name = "ipa.ipa" //默认包名 + var ipa_size = "" + var ipa_md5 = "" + + // 需要复制的文件 [fileName: fileURL] + var filePaths: [String: String] = [:] + + func createIAP(directoryPath: String) { + // 根标签 + let root = GDataXMLNode.element(withName: "package") + // package属性 + let version = GDataXMLNode.attribute(withName: "version", stringValue: "software5.11") as? GDataXMLNode + let xmlns = GDataXMLNode.attribute(withName: "xmlns", stringValue: "http://apple.com/itunes/importer") as? GDataXMLNode + root?.addAttribute(version) + root?.addAttribute(xmlns) + // provider\team_id + let pro = GDataXMLNode.element(withName: "provider", stringValue: provider) + let tid = GDataXMLNode.element(withName: "team_id", stringValue: team_id) + root?.addChild(pro) + root?.addChild(tid) + // software + // vendor_id + let software = GDataXMLNode.element(withName: "software") + let vid = GDataXMLNode.element(withName: "vendor_id", stringValue: vendor_id) + software?.addChild(vid) + // software_metadata/in_app_purchases + let software_metadata = GDataXMLNode.element(withName: "software_metadata") + let in_app_purchases = GDataXMLNode.element(withName: "in_app_purchases") + // in_app_purchase array + for iap in iaps { + let in_app_purchase = GDataXMLNode.element(withName: "in_app_purchase") + // product_id/reference_name/type + let product_id = GDataXMLNode.element(withName: "product_id", stringValue: iap.product_id) + let reference_name = GDataXMLNode.element(withName: "reference_name", stringValue: iap.reference_name) + let type = GDataXMLNode.element(withName: "type", stringValue: iap.type.rawValue) + // products + let products = GDataXMLNode.element(withName: "products") + let product = GDataXMLNode.element(withName: "product") + let cleared_for_sale = GDataXMLNode.element(withName: "cleared_for_sale", stringValue: "true") + let wholesale_price_tier = GDataXMLNode.element(withName: "wholesale_price_tier", stringValue: String(iap.wholesale_price_tier)) + product?.addChild(cleared_for_sale) + product?.addChild(wholesale_price_tier) + products?.addChild(product) + // locales + let locales = GDataXMLNode.element(withName: "locales") + let locale = GDataXMLNode.element(withName: "locale") + let name = GDataXMLNode.attribute(withName: "name", stringValue: iap.lang) as? GDataXMLNode + locale?.addAttribute(name) + let title = GDataXMLNode.element(withName: "title", stringValue: iap.title) + let description = GDataXMLNode.element(withName: "description", stringValue: iap.description) + locale?.addChild(title) + locale?.addChild(description) + locales?.addChild(locale) + // review_screenshot + let review_screenshot = GDataXMLNode.element(withName: "review_screenshot") + let file_name = GDataXMLNode.element(withName: "file_name", stringValue: iap.file_name) + let size = GDataXMLNode.element(withName: "size", stringValue: iap.size) + let checksum = GDataXMLNode.element(withName: "checksum", stringValue: iap.checksum) + let checksum_type = GDataXMLNode.attribute(withName: "type", stringValue: "md5") as? GDataXMLNode + checksum?.addAttribute(checksum_type) + review_screenshot?.addChild(file_name) + review_screenshot?.addChild(size) + review_screenshot?.addChild(checksum) + // review_notes + let review_notes = GDataXMLNode.element(withName: "review_notes", stringValue: iap.review_notes) + // 合并到in_app_purchase + in_app_purchase?.addChild(product_id) + in_app_purchase?.addChild(reference_name) + in_app_purchase?.addChild(type) + in_app_purchase?.addChild(products) + in_app_purchase?.addChild(locales) + in_app_purchase?.addChild(review_screenshot) + in_app_purchase?.addChild(review_notes) + in_app_purchases?.addChild(in_app_purchase) + } + software_metadata?.addChild(in_app_purchases) + software?.addChild(software_metadata) + root?.addChild(software) + // 生成xml文件 + let xmlDoc = GDataXMLDocument(rootElement: root) + let data = xmlDoc?.xmlData() +// let xmlString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue) + // 保存文件 + // 创建文件夹 + if !FileManager.default.fileExists(atPath: directoryPath) { + do { + try FileManager.default.createDirectory(atPath: directoryPath, withIntermediateDirectories: true, attributes: nil) + } catch { + print(error.localizedDescription); + } + } + // 创建文件 + let filePath = directoryPath + "/metadata.xml" + debugPrint(filePath) + if !FileManager.default.fileExists(atPath: filePath) { + FileManager.default.createFile(atPath: filePath, contents: data, attributes: nil) + }else { + try? FileManager.default.removeItem(atPath: filePath) + FileManager.default.createFile(atPath: filePath, contents: data, attributes: nil) + } + + for name in filePaths.keys { + let path = filePaths[name] ?? "" + try? FileManager.default.copyItem(atPath: path, toPath: directoryPath + "/" + name) + } + } + + + func createShots(directoryPath: String) { + // 根标签 + let root = GDataXMLNode.element(withName: "package") + // package属性 + let version = GDataXMLNode.attribute(withName: "version", stringValue: "software5.11") as? GDataXMLNode + let xmlns = GDataXMLNode.attribute(withName: "xmlns", stringValue: "http://apple.com/itunes/importer") as? GDataXMLNode + root?.addAttribute(version) + root?.addAttribute(xmlns) + // provider\team_id + let pro = GDataXMLNode.element(withName: "provider", stringValue: provider) + let tid = GDataXMLNode.element(withName: "team_id", stringValue: team_id) + root?.addChild(pro) + root?.addChild(tid) + // software + // vendor_id + let software = GDataXMLNode.element(withName: "software") + let vid = GDataXMLNode.element(withName: "vendor_id", stringValue: vendor_id) + software?.addChild(vid) + // software_metadata/in_app_purchases + let software_metadata = GDataXMLNode.element(withName: "software_metadata") + let app_platform = GDataXMLNode.attribute(withName: "app_platform", stringValue: app_platform) as? GDataXMLNode + software_metadata?.addAttribute(app_platform) + // versions + let s_versions = GDataXMLNode.element(withName: "versions") + let s_version = GDataXMLNode.element(withName: "version") + let app_version = GDataXMLNode.attribute(withName: "string", stringValue: app_version) as? GDataXMLNode + s_version?.addAttribute(app_version) + // locales + let locales = GDataXMLNode.element(withName: "locales") + let locale = GDataXMLNode.element(withName: "locale") + let locale_name = GDataXMLNode.attribute(withName: "name", stringValue: app_locale) as? GDataXMLNode + locale?.addAttribute(locale_name) + let locale_title = GDataXMLNode.element(withName: "title", stringValue: app_title) + locale?.addChild(locale_title) + + // app_previews + if videos.filter({ $0.value.count > 0 }).count > 0 { + let app_previews = GDataXMLNode.element(withName: "app_previews") + videos.forEach { (key: String, value: [Screen_Shot]) in + // app_preview + value.forEach { video in + let app_preview = GDataXMLNode.element(withName: "app_preview") + // display_target + let display_target = GDataXMLNode.attribute(withName: "display_target", stringValue: key) as? GDataXMLNode + app_preview?.addAttribute(display_target) + let position = GDataXMLNode.attribute(withName: "position", stringValue: video.position) as? GDataXMLNode + app_preview?.addAttribute(position) + // data_file + let data_file = GDataXMLNode.element(withName: "data_file") + let file_role = GDataXMLNode.attribute(withName: "role", stringValue: "source") as? GDataXMLNode + data_file?.addAttribute(file_role) + let file_size = GDataXMLNode.element(withName: "size", stringValue: video.size) + data_file?.addChild(file_size) + let file_name = GDataXMLNode.element(withName: "file_name", stringValue: video.file_name) + data_file?.addChild(file_name) + let checksum = GDataXMLNode.element(withName: "checksum", stringValue: video.checksum) + data_file?.addChild(checksum) + // data_file + let preview_image_time = GDataXMLNode.element(withName: "preview_image_time", stringValue: video.preview_time) + let preview_format = GDataXMLNode.attribute(withName: "format", stringValue: "30/1:1/nonDrop") as? GDataXMLNode + preview_image_time?.addAttribute(preview_format) + // 最后添加 + app_preview?.addChild(data_file) + app_preview?.addChild(preview_image_time) + app_previews?.addChild(app_preview) + } + + } + locale?.addChild(app_previews) + } + + if shots.filter({ $0.value.count > 0 }).count > 0 { + let app_screenshots = GDataXMLNode.element(withName: "software_screenshots") + shots.forEach { (key: String, value: [Screen_Shot]) in + // app_preview + value.forEach { video in + let app_screenshot = GDataXMLNode.element(withName: "software_screenshot") + // display_target + let display_target = GDataXMLNode.attribute(withName: "display_target", stringValue: key) as? GDataXMLNode + app_screenshot?.addAttribute(display_target) + let position = GDataXMLNode.attribute(withName: "position", stringValue: video.position) as? GDataXMLNode + app_screenshot?.addAttribute(position) + // data_file + let file_size = GDataXMLNode.element(withName: "size", stringValue: video.size) + app_screenshot?.addChild(file_size) + let file_name = GDataXMLNode.element(withName: "file_name", stringValue: video.file_name) + app_screenshot?.addChild(file_name) + let checksum = GDataXMLNode.element(withName: "checksum", stringValue: video.checksum) + app_screenshot?.addChild(checksum) + let checksum_type = GDataXMLNode.attribute(withName: "type", stringValue: "md5") as? GDataXMLNode + checksum?.addAttribute(checksum_type) + // 最后添加 + app_screenshots?.addChild(app_screenshot) + } + } + locale?.addChild(app_screenshots) + } + + locales?.addChild(locale) + s_version?.addChild(locales) + s_versions?.addChild(s_version) + software_metadata?.addChild(s_versions) + software?.addChild(software_metadata) + root?.addChild(software) + + // 生成xml文件 + let xmlDoc = GDataXMLDocument(rootElement: root) + let data = xmlDoc?.xmlData() + //let xmlString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue) + // 创建文件夹 + if !FileManager.default.fileExists(atPath: directoryPath) { + do { + try FileManager.default.createDirectory(atPath: directoryPath, withIntermediateDirectories: true, attributes: nil) + } catch { + print(error.localizedDescription); + } + } + + // 创建文件 + let filePath = directoryPath + "/metadata.xml" + debugPrint(filePath) + if !FileManager.default.fileExists(atPath: filePath) { + FileManager.default.createFile(atPath: filePath, contents: data, attributes: nil) + }else { + try? FileManager.default.removeItem(atPath: filePath) + FileManager.default.createFile(atPath: filePath, contents: data, attributes: nil) + } + + // 文件复制 + for name in filePaths.keys { + let path = filePaths[name] ?? "" + try? FileManager.default.copyItem(atPath: path, toPath: directoryPath + "/" + name) + } + } + + + func createIpaFile(directoryPath: String) { + // 根标签 + let root = GDataXMLNode.element(withName: "package") + // package属性 + let version = GDataXMLNode.attribute(withName: "version", stringValue: "software5.11") as? GDataXMLNode + let xmlns = GDataXMLNode.attribute(withName: "xmlns", stringValue: "http://apple.com/itunes/importer") as? GDataXMLNode + root?.addAttribute(version) + root?.addAttribute(xmlns) + // software_assets + let software_assets = GDataXMLNode.element(withName: "software_assets") + let apple_id = GDataXMLNode.attribute(withName: "apple_id", stringValue: apple_id) as? GDataXMLNode + let app_platform = GDataXMLNode.attribute(withName: "app_platform", stringValue: app_platform) as? GDataXMLNode + software_assets?.addAttribute(apple_id) + software_assets?.addAttribute(app_platform) + // asset + let asset = GDataXMLNode.element(withName: "asset") + let asset_type = GDataXMLNode.attribute(withName: "type", stringValue: archive_type) as? GDataXMLNode + asset?.addAttribute(asset_type) + //data_file + let data_file = GDataXMLNode.element(withName: "data_file") + let size = GDataXMLNode.element(withName: "size", stringValue: ipa_size) + let file_name = GDataXMLNode.element(withName: "file_name", stringValue: ipa_name) + let checksum = GDataXMLNode.element(withName: "checksum", stringValue: ipa_md5) + let checksum_type = GDataXMLNode.attribute(withName: "type", stringValue: "md5") as? GDataXMLNode + checksum?.addAttribute(checksum_type) + // 逆序添加 + data_file?.addChild(size) + data_file?.addChild(file_name) + data_file?.addChild(checksum) + asset?.addChild(data_file) + software_assets?.addChild(asset) + root?.addChild(software_assets) + + // 生成xml文件 + let xmlDoc = GDataXMLDocument(rootElement: root) + let data = xmlDoc?.xmlData() + let xmlString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue) + print(xmlString as Any) + // 创建文件夹 + if !FileManager.default.fileExists(atPath: directoryPath) { + do { + try FileManager.default.createDirectory(atPath: directoryPath, withIntermediateDirectories: true, attributes: nil) + } catch { + print(error.localizedDescription); + } + } + + // 创建文件 + let filePath = directoryPath + "/metadata.xml" + debugPrint(filePath) + if !FileManager.default.fileExists(atPath: filePath) { + FileManager.default.createFile(atPath: filePath, contents: data, attributes: nil) + }else { + try? FileManager.default.removeItem(atPath: filePath) + FileManager.default.createFile(atPath: filePath, contents: data, attributes: nil) + } + + // 文件复制 + for name in filePaths.keys { + let path = filePaths[name] ?? "" + try? FileManager.default.copyItem(atPath: path, toPath: directoryPath + "/" + name) + } + + } +} diff --git a/AppleParty/AppleParty/AppListView/InAppPurchseView/OutputExcelVC.swift b/AppleParty/AppleParty/AppListView/InAppPurchseView/OutputExcelVC.swift new file mode 100644 index 0000000..52d0f49 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/InAppPurchseView/OutputExcelVC.swift @@ -0,0 +1,67 @@ +// +// OutputExcelVC.swift +// AppleParty +// +// Created by 易承 on 2020/12/23. +// + +import Cocoa + +class OutputExcelVC: NSViewController, NSTextViewDelegate { + + @IBOutlet var inputText: NSTextView! + @IBOutlet weak var outputView: NSScrollView! + @IBOutlet var outputText: NSTextView! + + @IBOutlet weak var inputCount: NSTextField! + @IBOutlet weak var outputCount: NSTextField! + + var inputs = [String]() + var outputs = [String]() + + var iapList: [IAPList.IAP] = [] + + override func viewDidLoad() { + super.viewDidLoad() + inputText.delegate = self + } + + @IBAction func close(_ sender: Any) { + dismiss(self) + } + + @IBAction func commit(_ sender: Any) { + outputs.removeAll() + for i in 0.. 0 } + inputCount.stringValue = String(inputs.count)+"/100" + } + + func checkCount() -> Bool { + guard inputs.count == outputs.count else { + inputCount.textColor = NSColor.red + outputCount.textColor = NSColor.red + return false + } + inputCount.textColor = NSColor.lightGray + outputCount.textColor = NSColor.lightGray + return true + } +} diff --git a/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotHelpPopoverVC.swift b/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotHelpPopoverVC.swift new file mode 100644 index 0000000..c4af39a --- /dev/null +++ b/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotHelpPopoverVC.swift @@ -0,0 +1,18 @@ +// +// ScreenShotHelpPopoverVC.swift +// AppleParty +// +// Created by HTC on 2022/2/28. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class ScreenShotHelpPopoverVC: NSViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + } + +} diff --git a/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotHelpPopoverVC.xib b/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotHelpPopoverVC.xib new file mode 100644 index 0000000..20874a3 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotHelpPopoverVC.xib @@ -0,0 +1,1824 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +IAo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +- 5.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2208 x 1242 +- 6.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2688 x 1242 +- 12.9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2732 x 2048 +- macOS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2880 x 1800 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +IAo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +- 5.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1920 x 1080 +- 6.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1920 x 886 +- 12.9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1200 x 900 +- 12.9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1600 x 1200 +- macOS + + + + + + + + + + + + + + + + + + + + + + + + + 1920 x 1080 + + + + + + + + + + + + + + + 语言代码: +zh-Hans:中文(简体) +zh-Hant:中文(繁体) +ko:韩文 +ja:日文 + + + + + + + + + + + + + + + +Cg + + + + + + + + + + + + + + + + + + + + + + + + + + +1 + + + + + + + + + + + + + + + + + + + + + + + + + +2 + + + + + + + + + + + + + + + + + + + + + + + + + +3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 预览视频为可选项,可以不上传。 + + + + + + + + + + + + + + +Cg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotUpload.storyboard b/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotUpload.storyboard new file mode 100644 index 0000000..05b939d --- /dev/null +++ b/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotUpload.storyboard @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotUploadCell.swift b/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotUploadCell.swift new file mode 100644 index 0000000..53ee3ef --- /dev/null +++ b/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotUploadCell.swift @@ -0,0 +1,74 @@ +// +// ScreenShotUploadCell.swift +// AppleParty +// +// Created by HTC on 2022/2/25. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa +import Foundation + + +class ScreenShotDeleteCell: NSTableCellView { + + typealias CallFunc = (_ row: Int) -> Void + var deleteCell: CallFunc? + var row: Int = 0 + + @IBOutlet weak var deleteBtn: NSButton! + + @IBAction func clickedDeleteBtn(_ sender: NSButton) { + if let callBack = deleteCell { + callBack(row) + } + } +} + +class ScreenShotUploadCell: NSTableCellView { + + typealias CallBackHandler = (_ value: String, _ row: Int) -> Void + var changeSortIndex: CallBackHandler? + var changeVideoFrame: CallBackHandler? + var row: Int = 0 + + @IBOutlet weak var sortField: NSTextField! + @IBOutlet weak var videoField: NSTextField! + @IBOutlet weak var videoTitleField: NSTextField! + + @IBOutlet weak var cellTopConstraint: NSLayoutConstraint! + + override func awakeFromNib() { + super.awakeFromNib() + + sortField.delegate = self + videoField.delegate = self + } + + func updateData(sort: String, frame: String) { + sortField.stringValue = sort + videoField.stringValue = frame + } + + func showVideoView(_ show: Bool) { + videoField.isHidden = !show + videoTitleField.isHidden = !show + cellTopConstraint.constant = show ? 10.0 : 20.0 + } + +} + +extension ScreenShotUploadCell: NSTextFieldDelegate { + /// 内容改变 + func controlTextDidChange(_ obj: Notification) { + let textField = obj.object as! NSTextField + let value = textField.stringValue + if textField.tag == sortField.tag, let callBack = changeSortIndex { + callBack(value, row) + } + + if textField.tag == videoField.tag, let callBack = changeVideoFrame { + callBack(value, row) + } + } +} diff --git a/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotUploadVC.swift b/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotUploadVC.swift new file mode 100644 index 0000000..ae69288 --- /dev/null +++ b/AppleParty/AppleParty/AppListView/ScreenShotsView/ScreenShotUploadVC.swift @@ -0,0 +1,515 @@ +// +// ScreenShotUploadVC.swift +// AppleParty +// +// Created by HTC on 2022/2/25. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa +import AVFoundation + +enum ScreenShotType: Int { + case iOS5_5 = 0 //iPhone 5.5 英寸显示屏 + case iOS6_5 = 1 //iPhone 6.5 英寸显示屏 + case iPad_Pro = 2 //iPad Pro 12.9 英寸显示屏 +} + +class ScreenShotUploadVC: NSViewController { + + public var currentApp: App? { + didSet { + fetchAppInfo() + fetchAppVersionData() + } + } + public var appInfo: AppInfo? + + @IBOutlet weak var appName: NSTextField! + @IBOutlet weak var appSKU: NSTextField! + @IBOutlet weak var appleID: NSTextField! + @IBOutlet weak var localButton: NSPopUpButton! + + @IBOutlet weak var tableView: NSTableView! + @IBOutlet weak var tips_title: NSTextField! + @IBOutlet weak var tips_count: NSTextField! + @IBOutlet weak var tips_desc: NSTextField! + + private var selectTag = 0 + private var filesData = [[[String:String]]](repeating: [[String:String]](), count: 3) + + private let imageTypes = ["jpg", "jpeg", "png", "JPG", "JPEG", "PNG"] + private let videoTypes = ["mov", "m4v", "mp4", "MOV", "M4V", "MP4"] + private let locales = ["zh-Hans", "zh-Hant", "ko", "ja"] + + private let imageSizes = ["0": ["1242x2208", "2208x1242"], + "1": ["1242x2688", "2688x1242"], + "2": ["2048x2732", "2732x2048"]] + + private let videoSizes = ["0": ["1080x1920", "1920x1080"], + "1": ["886x1920", "1920x886"], + "2": ["1200x1600", "1600x1200"]] + + private var app_name = "" + private var app_version = "" + private var uploadModel = XMLModel() + + override func viewDidLoad() { + super.viewDidLoad() + + setupView() + } + + func setupView() { + + tableView.delegate = self + tableView.dataSource = self + tableView.selectionHighlightStyle = .none + + localButton.removeAllItems() + for locale in locales { + localButton.addItem(withTitle: locale) + } + localButton.selectItem(at: 0) + + let teamId = UserCenter.shared.developerTeamId + if teamId.isEmpty { + fetchAccountTeamInfo() + } else { + appleID.stringValue = "Team ID: " + UserCenter.shared.developerTeamId + } + } + + @IBAction func clickedUploadButton(_ sender: NSButton) { + + guard filesData.filter({ $0.count > 0 }).count > 0 else { + APHUD.hide(message: "图片或视频不能为空!", view: self.view, delayTime: 1) + return + } + + guard app_name.count > 0, app_version.count > 0 else { + APHUD.hide(message: "应用名和版本获取失败!请刷新重试~", view: self.view, delayTime: 1) + return + } + + guard let sp = UserCenter.shared.currentSPassword else { + let vc = APSPasswordSettingVC() + vc.updateCompletion = { [weak self] spassword in + if let sp = spassword { + self?.uploadData(sp) + } + } + presentAsSheet(vc) + return + } + + uploadData(sp) + } + + @IBAction func reloadAppData(_ sender: Any) { + fetchAppVersionData() + } + + @IBAction func clickedHelp(_ sender: NSButton) { + let vc = ScreenShotHelpPopoverVC() + self.present(vc, asPopoverRelativeTo: sender.frame, of: self.view, preferredEdge: .maxX, behavior:.transient) + } + + + @IBAction func changeSegmentedControl(_ sender: NSSegmentedControl) { + selectTag = sender.selectedTag() + reloadTableView(ScreenShotType.init(rawValue: selectTag) ?? .iOS5_5) + } + + @IBAction func uploadFiles(_ sender: NSButton) { + + let openPanel = NSOpenPanel() + openPanel.canChooseFiles = true + openPanel.canChooseDirectories = true + openPanel.allowsMultipleSelection = true + openPanel.allowedFileTypes = imageTypes + videoTypes + + openPanel.beginSheetModal(for: self.view.window!) { [self] (modalResponse) in + if modalResponse == .OK { + handleAutoImages(openPanel.urls) + } + } + } + +} + +// MARK: - 网络请求 +extension ScreenShotUploadVC { + + func fetchAccountTeamInfo() { + // 获取开发者 Team id 信息 + APClient.ascProvider.request { [weak self] result, response, error in + guard let err = error else { + self?.appleID.stringValue = "Team ID: " + UserCenter.shared.developerTeamId + return + } + APHUD.hide(message: err.localizedDescription, delayTime: 2) + } + } + + func fetchAppInfo(_ replay: Int = 3) { + guard let appid = currentApp?.appId else { + APHUD.hide(message: "当前 App 的 appleid 为空!", delayTime: 1) + return + } + + APClient.appInfo(appid: appid).request(showLoading: true) { [weak self] result, response, error in + if let err = error { + if replay > 0 { + self?.fetchAppInfo(replay-1) + } else { + NSAlert.show(err.localizedDescription) + } + return + } + let info = AppInfo(body: result) + self?.appInfo = info + self?.appSKU.stringValue = "App SKU: \(info.sku)" + } + + } + + func fetchAppVersionData(_ replay: Int = 3) { + + guard let appid = currentApp?.appId else { + APHUD.hide(message: "当前 App 的 appleid 为空!", delayTime: 1) + return + } + + APClient.appVersion(appid: appid).request { [weak self] data, response, error in + + if let err = error { + if replay > 0 { + self?.fetchAppVersionData(replay-1) + } else { + NSAlert.show(err.localizedDescription) + } + return + } + + let data = data["data"] as? [String: Any] + var dict = [String: Any]() + if let data = data, let platforms = data["platforms"] as? [[String: Any]] { + platforms.forEach { pf in + let platformString = pf["platformString"] as! String + dict[platformString] = pf + } + } + + guard let ios = dict["ios"] as? [String: Any], let inFlightVersion = ios["inFlightVersion"] as? [String: Any] else { + NSAlert.show("当前 App 无待送审的版本,请检查确认!") + return + } + + self?.app_version = inFlightVersion["version"] as! String + + if let data = data, let titles = data["localizedMetadata"] as? [[String: Any]] { + if titles.count > 0 { + // 这里只读取第一个 + self?.app_name = titles[0]["name"] as! String + } + } + + DispatchQueue.main.async { [self] in + self?.appName.stringValue = "App Name: \(self!.app_name) (\(self!.app_version))" + } + + } + } +} + + +// MARK: - NSTableViewDelegate +extension ScreenShotUploadVC: NSTableViewDelegate, NSTableViewDataSource { + func numberOfRows(in tableView: NSTableView) -> Int { + return filesData[selectTag].count + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + + var data = filesData[selectTag][row] + let isImg = data["type"]! == "1" + + switch tableColumn!.identifier.rawValue { + case "review": + let imgView = NSImageView() + if let imageRef = NSImage(byReferencingFile: data["url"]!) { + imgView.image = imageRef + } + if !isImg { + let url = URL(fileURLWithPath: data["url"]!) + let videoImg = previewImageForLocalVideo(url) + imgView.image = videoImg + } + return imgView + case "fileName": + let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "fileName"), owner: self) as? NSTableCellView + cell?.textField?.stringValue = "\(isImg ? "图片" : "视频"):\n\(data["name"]!)" + return cell + case "setting": + let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "setting"), owner: self) as? ScreenShotUploadCell + cell?.row = row + cell?.showVideoView(!isImg) + cell?.updateData(sort: data["index"] ?? "", frame: data["frame"] ?? "00:00") + cell?.changeSortIndex = { index, crow in + data["index"] = index + self.filesData[self.selectTag][row] = data + } + cell?.changeVideoFrame = { frame, crow in + data["frame"] = frame + self.filesData[self.selectTag][row] = data + } + return cell + case "operation": + let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "operation"), owner: self) as? ScreenShotDeleteCell + cell?.row = row + cell?.deleteCell = { row in + self.filesData[self.selectTag].remove(at: row) + self.tableView.reloadData() + self.reloadTableView(ScreenShotType(rawValue: self.selectTag) ?? .iOS5_5) + } + return cell + default: + return nil + } + } + + func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { + 80 + } + + func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { + // macOS 11 以下,如果不能点击,textfield 也不能点击 + return true + } +} + + +// MARK: - Upload +extension ScreenShotUploadVC { + + func uploadData(_ sp: SPassword) { + + APHUD.show(message: "上传中", view: self.view) + + let localIndex = localButton.indexOfSelectedItem + + DispatchQueue.global(qos: .userInitiated).async { [self] in + + uploadModel = XMLModel() + + guard let info = appInfo else { + fetchAppInfo() + APHUD.hide(message: "加载数据异常,请刷新后重试~") + return + } + + uploadModel.vendor_id = info.sku + + // 获取创建 itms 文件的路径 + let filePath = XMLManager.getShotsPath(currentApp!.appId) + + // 先删除旧的文档 + XMLManager.deleteITMS(filePath) + + // 数据转模型 + handleDataToModel(localIndex) + + uploadModel.createShots(directoryPath: filePath) + + let result = XMLManager.uploadITMS(account: sp.account, pwd: sp.password, filePath: filePath) + + DispatchQueue.main.async { + APHUD.hide() + self.closeSelfAndCallBack(result) + } + } + } + + func handleDataToModel(_ localIndex: Int) { + + let locale = locales[localIndex] + uploadModel.app_locale = locale + uploadModel.app_title = app_name + uploadModel.app_version = app_version + + let iOS5_5 = filesData[0] + let iOS6_5 = filesData[1] + let iPad = filesData[2] + + //iPhone 5.5 英寸 + let (img5_5, video5_5, set5_5) = getScreenShotModel(iOS5_5) + uploadModel.shots["iOS-5.5-in"] = img5_5 + uploadModel.videos["iOS-5.5-in"] = video5_5 + + //iPhone 6.5 英寸 + let (img6_5, video6_5, set6_5) = getScreenShotModel(iOS6_5) + uploadModel.shots["iOS-6.5-in"] = img6_5 + uploadModel.videos["iOS-6.5-in"] = video6_5 + + let (imgiPad, videoiPad, setiPad) = getScreenShotModel(iPad) + //iPad Pro(第2代) 12.9 英寸 + uploadModel.shots["iOS-iPad-Pro"] = imgiPad + uploadModel.videos["iOS-iPad-Pro"] = videoiPad + //iPad Pro(第3代) 12.9 英寸 + uploadModel.shots["iOS-iPad-Pro-2018"] = imgiPad + uploadModel.videos["iOS-iPad-Pro-2018"] = videoiPad + + // 图片和视频资源 + let fileURLs = set5_5.union(set6_5).union(setiPad) + var dict = [String: String]() + fileURLs.forEach { value in + value.forEach { dict[$0] = $1 } + } + uploadModel.filePaths = dict + } + + func getScreenShotModel(_ shots: [[String: String]]) -> ([Screen_Shot], [Screen_Shot], Set<[String:String]>) { + var imageList = [Screen_Shot]() + var videoList = [Screen_Shot]() + var urlList: Set<[String:String]> = [] + shots.forEach { data in + let isImg = data["type"]! == "1" + let url = data["url"] ?? "" + let kind = data["kind"] ?? "" + let name = kind + (data["name"] ?? "") //kind+name:避免不同尺寸使用同一个图片名字,导致替换的问题 + let index = data["index"] ?? "" + let frame = "00:00:" + (data["frame"] ?? "00:00") + let size = URL.init(fileURLWithPath: url).fileSize() + let md5 = URL.init(fileURLWithPath: url).fileMD5() + let item = Screen_Shot(file_name: name, size: size, checksum: md5 ?? "", position: index, preview_time: frame) + urlList.insert([name: url]) + isImg ? imageList.append(item) : videoList.append(item) + } + return (imageList, videoList, urlList) + } + + func closeSelfAndCallBack(_ result: (Int32, String?)) { + + if result.0 == 0 { + NSAlert.show("上传成功!稍后可在苹果后台查看~") + // 删除旧的文档,避免占用空间过大 + let filePath = XMLManager.getShotsPath(currentApp!.appId) + XMLManager.deleteITMS(filePath) + } else { + let sb = NSStoryboard(name: "APDebugVC", bundle: Bundle(for: self.classForCoder)) + let newWC = sb.instantiateController(withIdentifier: "APDebugWC") as? NSWindowController + let logVC = newWC?.contentViewController as? APDebugVC + newWC?.window?.title = "上传错误日志" + logVC?.debugLog = result.1 ?? "" + newWC?.showWindow(self) + } + } +} + +// MARK: - 内部方法 +extension ScreenShotUploadVC { + + func handleAutoImages(_ files: [URL]) { + files.forEach { url in + // 如果是文件夹,则递归 + if url.hasDirectoryPath { + let urls = subFilesInDirectory(url: url) + handleAutoImages(urls) + return + } + var size = "0x0" + var sizes = [String: [String]]() + var type = "1" + let fileType = url.pathExtension + if imageTypes.contains(fileType) { + let image = NSImage(contentsOf: url) + guard let rep = image?.representations.first as? NSBitmapImageRep else { + return + } + size = "\(rep.pixelsWide)x\(rep.pixelsHigh)" + sizes = imageSizes + type = "1" + } else if videoTypes.contains(fileType) { + guard let track = AVURLAsset(url: url).tracks(withMediaType: AVMediaType.video).first else { + return + } + let rep = track.naturalSize.applying(track.preferredTransform) + size = "\(Int(rep.width))x\(Int(rep.height))" + sizes = videoSizes + type = "2" + } + + sizes.forEach { (index: String, value: [String]) in + if value.contains(size) { + // 处理数据 + var dict = [String: String]() + dict["url"] = url.path + dict["name"] = url.lastPathComponent + dict["type"] = type + dict["kind"] = index + // 更新数据 + var value = filesData[Int(index)!] + value.append(dict) + filesData[Int(index)!] = value + } + } + } + // 刷新列表 + reloadTableView(ScreenShotType.init(rawValue: selectTag) ?? .iOS5_5) + } + + func subFilesInDirectory(url: URL) -> [URL] { + var urls = [URL]() + // 迭代器,包含子目录 + let files = FileManager.default.enumerator(atPath: url.path) + while let file = files?.nextObject() { + if let file = file as? String { + let nextFile = url.appendingPathComponent(file) + if !nextFile.hasDirectoryPath { + urls.append(nextFile) + } + } + } + return urls + } + + func reloadTableView(_ index: ScreenShotType) { + let list = filesData[index.rawValue] + let images = list.filter({ $0["type"] == "1" }) + tips_count.stringValue = "\(list.count - images.count)/3 个 App 预览 | \(images.count)/10 张屏幕快照" + switch index { + case .iOS5_5: + tips_title.stringValue = "iPhone 5.5 英寸显示屏" + tips_desc.stringValue = "图片:1242 x 2208、2208 x 1242,视频:1080 x 1920、1920 x 1080" + case .iOS6_5: + tips_title.stringValue = "iPhone 6.5 英寸显示屏" + tips_desc.stringValue = "图片:1242 x 2688、2688 x 1242,视频:886 x 1920、1920 x 886" + case .iPad_Pro: + tips_title.stringValue = "iPad Pro 12.9 英寸显示屏" + tips_desc.stringValue = "图片:2048 x 2732、2732 x 2048,视频:1200 x 1600、1600 x 1200" + } + + tableView.reloadData() + } + + func previewImageForLocalVideo(_ url: URL) -> NSImage? { + let asset = AVAsset(url: url) + let imageGenerator = AVAssetImageGenerator(asset: asset) + imageGenerator.appliesPreferredTrackTransform = true + var time = asset.duration + //If possible - take not the first frame (it could be completely black or white on camara's videos) + time.value = min(time.value, 2) + do { + let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil) + let track = asset.tracks(withMediaType: AVMediaType.video).first + let rep = track?.naturalSize.applying(track?.preferredTransform ?? CGAffineTransform()) + return NSImage(cgImage: imageRef, size: NSSize(width: rep?.width ?? 120, height: rep?.height ?? 60)) + } + catch let error as NSError + { + print("Image generation failed with error \(error)") + return nil + } + } +} diff --git a/AppleParty/AppleParty/AppListView/zh-Hans.lproj/AppList.strings b/AppleParty/AppleParty/AppListView/zh-Hans.lproj/AppList.strings new file mode 100644 index 0000000..57ae3cb --- /dev/null +++ b/AppleParty/AppleParty/AppListView/zh-Hans.lproj/AppList.strings @@ -0,0 +1,3 @@ + +/* Class = "NSWindow"; title = "My Apps"; ObjectID = "mcp-fI-0bQ"; */ +"mcp-fI-0bQ.title" = "我的 App"; diff --git a/AppleParty/AppleParty/AppSettingView/APSettingVC.swift b/AppleParty/AppleParty/AppSettingView/APSettingVC.swift new file mode 100644 index 0000000..e66407d --- /dev/null +++ b/AppleParty/AppleParty/AppSettingView/APSettingVC.swift @@ -0,0 +1,49 @@ +// +// SettingVC.swift +// AppleParty +// +// Created by HTC on 2022/3/25. +// Copyright © 2021 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APSettingVC: NSViewController { + + var isLoginViewShow: Bool { + get { return false } + set { + sPasswordBtn.isHidden = newValue + clearCacheBtn.isHidden = !newValue + } + } + + @IBOutlet weak var trusDeviceBtn: NSButton! + @IBOutlet weak var sPasswordBtn: NSButton! + @IBOutlet weak var clearCacheBtn: NSButton! + + @IBAction func clickedTrusDeviceBtn(_ sender: NSButton) { + InfoCenter.shared.trusDevice = sender.state == .on ? true : false + } + + @IBAction func clickedSPasswordBtn(_ sender: Any) { + let vc = APSPasswordSettingVC() + presentAsSheet(vc) + } + + + @IBAction func clickedClearCacheBtn(_ sender: NSButton) { + // 清掉缓存 + HTTPCookieStorage.shared.cookies?.forEach(HTTPCookieStorage.shared.deleteCookie) + InfoCenter.shared.cookies = [] + APHUD.hide(message: "清掉缓存成功", view: self.view) + } + + + override func viewDidLoad() { + super.viewDidLoad() + title = "App设置" + trusDeviceBtn.state = InfoCenter.shared.trusDevice ? .on : .off + } + +} diff --git a/AppleParty/AppleParty/AppSettingView/APSettingVC.xib b/AppleParty/AppleParty/AppSettingView/APSettingVC.xib new file mode 100644 index 0000000..d4539bf --- /dev/null +++ b/AppleParty/AppleParty/AppSettingView/APSettingVC.xib @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/AppleParty-Bridging-Header.h b/AppleParty/AppleParty/AppleParty-Bridging-Header.h new file mode 100644 index 0000000..32e051f --- /dev/null +++ b/AppleParty/AppleParty/AppleParty-Bridging-Header.h @@ -0,0 +1,7 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "GDataXMLNode.h" +#import "QrcodeUtil.h" +#import "MBProgressHUD.h" diff --git a/AppleParty/AppleParty/AppleParty.entitlements b/AppleParty/AppleParty/AppleParty.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/AppleParty/AppleParty/AppleParty.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/AppleParty/AppleParty/EmailToolView/EmailSettingVC.swift b/AppleParty/AppleParty/EmailToolView/EmailSettingVC.swift new file mode 100644 index 0000000..4e1627e --- /dev/null +++ b/AppleParty/AppleParty/EmailToolView/EmailSettingVC.swift @@ -0,0 +1,61 @@ +// +// EmailSettingVC.swift +// AppleParty +// +// Created by HTC on 2022/3/29. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + + +struct EamilConfigs { + var name: String = "" + var addr: String = "" + var pwd: String = "" + var smtp: String = "" +} + +var eamilConfigs: EamilConfigs? { + get { + let value = (try? APUtil.keychain.getString("APEmailSetting_Key")) ?? "" + let arr = value.components(separatedBy: "|") + guard arr.count == 4 else { + return nil + } + return EamilConfigs(name: arr[0], addr: arr[1], pwd: arr[2], smtp: arr[3]) + } + set { + guard let newValue = newValue else { return } + let value = "\(newValue.name)|\(newValue.addr)|\(newValue.pwd)|\(newValue.smtp)" + try? APUtil.keychain.set(value, key: "APEmailSetting_Key") + } +} + +class EmailSettingVC: NSViewController { + + public var closeHandle: (() -> Void)? + + @IBOutlet weak var emailNameView: NSTextField! + @IBOutlet weak var emailAddrView: NSTextField! + @IBOutlet weak var emailPwdView: NSTextField! + @IBOutlet weak var emailSMTPView: NSTextField! + + + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + } + + @IBAction func clickedCancelBtn(_ sender: Any) { + closeHandle?() + } + + @IBAction func clickedSubmitBtn(_ sender: Any) { + + eamilConfigs = EamilConfigs(name: emailNameView.stringValue, addr: emailAddrView.stringValue, pwd: emailPwdView.stringValue, smtp: emailSMTPView.stringValue) + closeHandle?() + } + +} diff --git a/AppleParty/AppleParty/EmailToolView/EmailSettingVC.xib b/AppleParty/AppleParty/EmailToolView/EmailSettingVC.xib new file mode 100644 index 0000000..e781101 --- /dev/null +++ b/AppleParty/AppleParty/EmailToolView/EmailSettingVC.xib @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/EmailToolView/EmailTool.storyboard b/AppleParty/AppleParty/EmailToolView/EmailTool.storyboard new file mode 100644 index 0000000..751a1a1 --- /dev/null +++ b/AppleParty/AppleParty/EmailToolView/EmailTool.storyboard @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/EmailToolView/EmailToolVC.swift b/AppleParty/AppleParty/EmailToolView/EmailToolVC.swift new file mode 100644 index 0000000..2a48aac --- /dev/null +++ b/AppleParty/AppleParty/EmailToolView/EmailToolVC.swift @@ -0,0 +1,247 @@ +// +// EmailToolVC.swift +// AppleParty +// +// Created by iHTC on 20211025. +// Copyright © 2021 37 Mobile Games. All rights reserved. +// + +import Cocoa + + +class EmailToolVC: NSViewController { + + var emailTitle: String? { + didSet { + if let text = emailTitle { + emailTitleTF.stringValue = text + } + } + } + + var emailContent: String? { + didSet { + if let text = emailContent { + emailContentTextView.string = text + } + } + } + + var attachmentFileUrl: URL? { + didSet { + if let url = attachmentFileUrl { + fileURLs?.append(url) + fileDropZoneView.setFile(url) + } + } + } + + @IBOutlet weak var emailRecipientTF: NSTextField! + @IBOutlet weak var rememberEmailButton: NSButton! + @IBOutlet weak var emailTitleTF: NSTextField! + @IBOutlet weak var emailSendButton: NSButton! + @IBOutlet weak var emailContentTextView: NSTextView! + @IBOutlet weak var emialContentView: NSScrollView! + @IBOutlet weak var multipleFilesButton: NSButton! + @IBOutlet weak var selectFilesButton: NSButton! + @IBOutlet weak var clearnAllFilesButton: NSButton! + @IBOutlet weak var selectFilesView: NSScrollView! + @IBOutlet weak var selectilesTextView: NSTextView! + + private var fileDropZoneView = DropZoneView(fileTypes: [], text: "点击或拖拽文件到这里") + private var fileURLs: [URL]? + // 邮件地址 + private var emailsString: String { + get { string(from: UserDefaults.standard.object(forKey: "EmailToolVC_RememberEmailString")) } + set { UserDefaults.standard.setValue(newValue, forKey: "EmailToolVC_RememberEmailString") } + } + + private lazy var settingPopover: NSPopover = { + let settingPopover = NSPopover() + let vc = EmailSettingVC() + vc.closeHandle = { + settingPopover.close() + } + settingPopover.contentViewController = vc + return settingPopover + }() + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + + func setupUI() { + + if emailsString.count > 0 { + emailRecipientTF.stringValue = emailsString + rememberEmailButton.state = .on + } else { + rememberEmailButton.state = .off + } + + fileDropZoneView.translatesAutoresizingMaskIntoConstraints = false + fileDropZoneView.delegate = self + view.addSubview(fileDropZoneView) + fileDropZoneView.snp.makeConstraints { (make) in + make.top.equalTo(multipleFilesButton.snp.bottom).offset(15) + make.left.equalToSuperview().offset(20) + make.right.equalToSuperview().offset(-20) + make.bottom.equalToSuperview().offset(-30) + } + + } + + + func validEmail() -> [String]? { + + let recipient = emailRecipientTF.stringValue + guard recipient.count > 0 else { + APHUD.hide(message: "收件人邮箱不能为空!", view: self.view, delayTime: 2) + return nil + } + + let allEmails = recipient.components(separatedBy: [";", ";", ",", ","]).filter({!$0.isEmpty}) + let emails = allEmails.filter({ isEmailValid($0) }) + if emails.isEmpty { + APHUD.hide(message: "收件人邮箱格式不正确!", view: self.view, delayTime: 2) + return nil + } + + return emails + } + + @IBAction func clickedEmailSettingButton(_ sender: NSButton) { + + if settingPopover.isShown { + settingPopover.performClose(self) + }else { + settingPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.minY) + } + } + + + @IBAction func sendEmailButton(_ sender: NSButton) { + guard let config = eamilConfigs else { + APHUD.hide(message: "请先设置邮箱服务器信息!", view: self.view, delayTime: 2) + return + } + + // 收件人 + guard let emails = validEmail() else { + return + } + + sender.isEnabled = false + + APHUD.show(message: "发送中...", view: self.view) + + var title = emailTitleTF.stringValue + if title.isEmpty { + title = "邮件助手" + } + + let contents = "

\(emailContentTextView.textStorage?.string ?? "")

".replacingOccurrences(of: "\n", with: "
") + + var files = [String]() + fileURLs?.forEach({ url in + files.append(url.path) + }) + + // 发送邮件 + EmailUtils.autoSendAtts(subject: "AppleParty — \(title)", recipients: emails, htmlContent: contents, attachmentFiles: files, config: config) { error in + DispatchQueue.main.async { + sender.isEnabled = true + APHUD.hide() + debugPrint(error as Any) + var msg = "邮箱发送成功~" + if let err = error { + msg = "邮箱发送失败:\(String(describing: err))" + } + NSAlert.show(msg) + } + } + } + + @IBAction func rememberEmail(_ sender: Any) { + // 收件人 + guard validEmail() != nil else { + return + } + + emailsString = emailRecipientTF.stringValue + } + + @IBAction func ChangeMultipleFiles(_ sender: NSButton) { + + clearnAllFiles(clearnAllFilesButton) + + if sender.state == .on { + fileDropZoneView.isHidden = true + selectFilesButton.isHidden = false + clearnAllFilesButton.isHidden = false + selectFilesView.isHidden = false + + } else { + fileDropZoneView.reset() + fileDropZoneView.isHidden = false + selectFilesButton.isHidden = true + clearnAllFilesButton.isHidden = true + selectFilesView.isHidden = true + } + } + + @IBAction func clearnAllFiles(_ sender: NSButton) { + fileURLs = [] + selectilesTextView.string = "" + } + + @IBAction func selectFiles(_ sender: Any) { + let openPanel = NSOpenPanel() + openPanel.canChooseFiles = true + openPanel.canChooseDirectories = false + openPanel.allowsMultipleSelection = true + + openPanel.beginSheetModal(for: self.view.window!) { [self] (modalResponse) in + if modalResponse == .OK { + self.fileURLs?.append(contentsOf: openPanel.urls) + self.updateFilesView() + } + } + } + + func updateFilesView() { + fileURLs?.forEach({ file in + let path = file.lastPathComponent + selectilesTextView.string.append(path + "\n") + }) + + selectilesTextView.scrollRangeToVisible(NSMakeRange(selectilesTextView.string.count, 0)) + } +} + + +// MARK: - DropZoneViewDelegate +extension EmailToolVC: DropZoneViewDelegate { + + func receivedFile(dropZoneView: DropZoneView, fileURL: URL) { + fileURLs = [fileURL] + } + + func receivedMouseDown(dropZoneView: DropZoneView, theEvent: NSEvent) { + let openPanel = NSOpenPanel() + openPanel.canChooseFiles = true + openPanel.canChooseDirectories = false + openPanel.allowsMultipleSelection = false + + openPanel.beginSheetModal(for: self.view.window!) { (modalResponse) in + if modalResponse == .OK { + if let fileURL = openPanel.url { + self.fileURLs = [fileURL] + dropZoneView.setFile(fileURL) + } + } + } + } +} diff --git a/AppleParty/AppleParty/IPAUpload/APIPAUploadVC.swift b/AppleParty/AppleParty/IPAUpload/APIPAUploadVC.swift new file mode 100644 index 0000000..358c404 --- /dev/null +++ b/AppleParty/AppleParty/IPAUpload/APIPAUploadVC.swift @@ -0,0 +1,174 @@ +// +// APIPAUploadVC.swift +// AppleParty +// +// Created by HTC on 2022/5/12. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APIPAUploadVC: NSViewController { + + @IBOutlet weak var appIdTextView: NSTextField! + @IBOutlet weak var appIdTextField: NSTextField! + @IBOutlet weak var spasswordLbl: NSTextField! + @IBOutlet weak var submitBtn: NSButton! + + //通过外界传入的 apple id时,不需要用户填写 + var apple_id: String? { + didSet { + if let appId = apple_id { + appIdTextView.stringValue = appId + appIdTextView.isHidden = false + appIdTextField.isHidden = true + } else { + appIdTextView.stringValue = "" + appIdTextView.isHidden = true + appIdTextField.isHidden = false + } + } + } + + private var ipaFileURL: URL? + private var fileDropZoneView = DropZoneView(fileTypes: [".ipa"], text: "点击或拖拽IPA到这里") + private var uploadModel = XMLModel() + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + updateSPasswordUI() + } + + func setupUI() { + + fileDropZoneView.translatesAutoresizingMaskIntoConstraints = false + fileDropZoneView.delegate = self + view.addSubview(fileDropZoneView) + fileDropZoneView.snp.makeConstraints { (make) in + make.top.equalTo(submitBtn.snp.bottom).offset(15) + make.left.equalToSuperview().offset(20) + make.right.equalToSuperview().offset(-20) + make.bottom.equalToSuperview().offset(-30) + } + } + + func updateSPasswordUI() { + if let sp = UserCenter.shared.currentSPassword { + spasswordLbl.stringValue = "(当前选择:\(sp.account))" + } else { + spasswordLbl.stringValue = "(错误:当前未指定专用密码!)" + } + } + + @IBAction func clickedSPasswordBtn(_ sender: NSButton) { + let vc = APSPasswordSettingVC() + vc.updateCompletion = { [weak self] ps in + self?.updateSPasswordUI() + } + presentAsSheet(vc) + } + + @IBAction func clickedSubmitBtn(_ sender: NSButton) { + uploadIpaFile() + } + +} + +// MARK: - Private Method +extension APIPAUploadVC { + + private func uploadIpaFile() { + + var appId = appIdTextField.stringValue + if let appleId = apple_id { + appId = appleId + } + guard !appId.isEmpty else { + APHUD.hide(message: "请先填写 app id ~", delayTime: 1) + return + } + + guard let sp = UserCenter.shared.currentSPassword else { + let vc = APSPasswordSettingVC() + vc.updateCompletion = { [weak self] spassword in + self?.uploadIpaFile() + } + presentAsSheet(vc) + APHUD.hide(message: "请先设置或指定专用密码~", delayTime: 1) + return + } + + guard let ipaFileURL = ipaFileURL else { + APHUD.hide(message: "请先上传 ipa 文件~", delayTime: 1) + return + } + + APHUD.show(message: "上传中", view: self.view) + + DispatchQueue.global(qos: .userInitiated).async { [self] in + + uploadModel = XMLModel() + uploadModel.apple_id = appId + uploadModel.ipa_size = ipaFileURL.fileSize() + uploadModel.ipa_md5 = ipaFileURL.fileMD5() ?? "" + uploadModel.filePaths = ["ipa.ipa": ipaFileURL.path] + + // 获取创建 itms 文件的路径 + let filePath = XMLManager.getIpaPath(appId) + + // 先删除旧的文档 + XMLManager.deleteITMS(filePath) + + uploadModel.createIpaFile(directoryPath: filePath) + + let result = XMLManager.uploadITMS(account: sp.account, pwd: sp.password, filePath: filePath) + + DispatchQueue.main.async { + APHUD.hide() + self.closeSelfAndCallBack(result) + } + } + + } + + + func closeSelfAndCallBack(_ result: (Int32, String?)) { + if result.0 == 0 { + NSAlert.show("ipa文件上传成功!稍后可在苹果后台查看~") + }else { + let sb = NSStoryboard(name: "APDebugVC", bundle: Bundle(for: self.classForCoder)) + let newWC = sb.instantiateController(withIdentifier: "APDebugWC") as? NSWindowController + let logVC = newWC?.contentViewController as? APDebugVC + newWC?.window?.title = "ipa上传错误日志" + logVC?.debugLog = result.1 ?? "" + newWC?.showWindow(self) + } + } +} + + +// MARK: - DropZoneViewDelegate +extension APIPAUploadVC: DropZoneViewDelegate { + + func receivedFile(dropZoneView: DropZoneView, fileURL: URL) { + ipaFileURL = fileURL + } + + func receivedMouseDown(dropZoneView: DropZoneView, theEvent: NSEvent) { + let openPanel = NSOpenPanel() + openPanel.canChooseFiles = true + openPanel.canChooseDirectories = false + openPanel.allowsMultipleSelection = false + openPanel.allowedFileTypes = ["ipa"] + + openPanel.beginSheetModal(for: self.view.window!) { (modalResponse) in + if modalResponse == .OK { + if let fileURL = openPanel.url { + self.ipaFileURL = fileURL + dropZoneView.setFile(fileURL) + } + } + } + } +} diff --git a/AppleParty/AppleParty/IPAUpload/IPAUpload.storyboard b/AppleParty/AppleParty/IPAUpload/IPAUpload.storyboard new file mode 100644 index 0000000..aedab52 --- /dev/null +++ b/AppleParty/AppleParty/IPAUpload/IPAUpload.storyboard @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/Info.plist b/AppleParty/AppleParty/Info.plist new file mode 100644 index 0000000..3758032 --- /dev/null +++ b/AppleParty/AppleParty/Info.plist @@ -0,0 +1,15 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + SUFeedURL + https://raw.githubusercontent.com/37iOS/AppleParty/main/AppleParty/SparkleUpdate/update.xml + SUPublicEDKey + ItVUtr4L9w9VfMGlzg7+cIcvSkruiygDcarlq8PTF7I= + + diff --git a/AppleParty/AppleParty/LoginView/APLogin2FAVC.swift b/AppleParty/AppleParty/LoginView/APLogin2FAVC.swift new file mode 100644 index 0000000..76dbdd0 --- /dev/null +++ b/AppleParty/AppleParty/LoginView/APLogin2FAVC.swift @@ -0,0 +1,199 @@ +// +// APLogin2FAVC.swift +// AppleParty +// +// Created by HTC on 2022/3/17. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APLogin2FAVC: NSViewController { + + public var cancelHandle: (() -> Void)? + public var successHandle: (() -> Void)? + + @IBOutlet weak var phoneListBtn: NSPopUpButton! + @IBOutlet weak var sendCodeBtn: NSButton! + @IBOutlet weak var voiceCodeBtn: NSButton! + @IBOutlet weak var phoneCodeView: NSTextField! + @IBOutlet weak var tipsWarningView: NSTextField! + @IBOutlet weak var trusDeviceBtn: NSButton! + @IBOutlet weak var indicatorView: NSProgressIndicator! + @IBOutlet weak var verifyBtn: NSButton! + + private var numbers: [PNumber] = [] //验证手机号码列表 + private var isPhoneSecurity = false //是否通过手机验证码来验证 + // 验证码倒计时 + private var verifyCodeTimer: Timer? + private var lastTime: Int = 30 + + override func viewDidLoad() { + super.viewDidLoad() + phoneCodeView.delegate = self + fetchPhoneList() + trusDeviceBtn.state = InfoCenter.shared.trusDevice ? .on : .off + } + + @IBAction func clickedCancelBtn(_ sender: NSButton) { + closeView() + cancelHandle?() + } + + @IBAction func clickedSendCodeBtn(_ sender: NSButton) { + submitSecurityCode() + } + + + @IBAction func changeVoiceCodeBtn(_ sender: NSButton) { + sendCodeBtn.title = sender.state == .on ? "拨打语音验证码" : "发送短信验证码" + } + + + @IBAction func clickedVerifyBtn(_ sender: NSButton) { + verifySecurityCode() + } + + @IBAction func clickedTrusDeviceBtn(_ sender: NSButton) { + InfoCenter.shared.trusDevice = sender.state == .on ? true : false + } +} + + +// MARK: - 网络请求 +extension APLogin2FAVC { + + func fetchPhoneList() { + APClient.verifySecurityPhone(mode: "sms", phoneid: 0).request(showLoading: true) { [weak self] result, response, error in + if let err = error, let type = APClientErrorCode(rawValue: err.code) { + switch type { + case .privacyAcknowledgementRequired: + // 传了无效phoneid,进入选择手机号的流程 + self?.phoneListBtn.removeAllItems() + let model = PhoneNumbers(body: result) + self?.numbers = model.numbers + for number in model.numbers { + self?.phoneListBtn.addItem(withTitle: number.num) + } + self?.phoneListBtn.selectItem(at: 0) + self?.showTips("一条包含验证码的信息已发送至您的设备。可输入设备验证码后点击验证以继续。\n或者点击“发送短信验证码”获取短信验证码。") + default: + APHUD.hide(message: err.localizedDescription) + } + } + } + } + + func submitSecurityCode() { + isPhoneSecurity = true + let mode = voiceCodeBtn.state == .on ? "voice" : "sms" + let phoneId = numbers[phoneListBtn.indexOfSelectedItem].id + APClient.verifySecurityPhone(mode: mode, phoneid: phoneId).request(showLoading: true) { [weak self] result, response, error in + let code = response?.statusCode + if [200, 423].contains(code) { + let msg = self?.voiceCodeBtn.state == .on ? "请求拨打语音电话,请收听~" : "验证码已发送,请查收~" + self?.showTips(msg) + self?.verifyCodeCountdown() + } else { + self?.showTips("\(code ?? 0),\(error.debugDescription)") + } + } + } + + func verifySecurityCode() { + let code = phoneCodeView.stringValue + let phoneId = numbers[phoneListBtn.indexOfSelectedItem].id + let mode = voiceCodeBtn.state == .on ? "voice" : "sms" + let type = isPhoneSecurity ? APClient.SecurityCode.sms(code: code, phoneNumberId: phoneId, mode: mode) : APClient.SecurityCode.device(code: code) + + viewEnabled(false) + APClient.submitSecurityCode(code: type).request { [weak self] result, response, error in + self?.viewEnabled(true) + let code = response?.statusCode + switch code { + case 200, 201, 202, 203, 204: + self?.validateSession() + case 400: + let errors = dictionaryArray(result["service_errors"]) + let msg = string(from: errors.first?["message"]) + self?.showTips(msg) + default: + self?.showTips("\(code ?? 0),\(error.debugDescription)") + } + } + } + + func validateSession() { + viewEnabled(false) + APClient.signInSession.request { [weak self] result, response, error in + self?.viewEnabled(true) + let code = response?.statusCode + switch code { + case 200, 201: + UserCenter.shared.isAuthorized = true + self?.successHandle?() + self?.closeView() + default: + let errors = dictionaryArray(result["serviceErrors"]) + let msg = string(from: errors.first?["message"]) + self?.showTips(msg.isEmpty ? error.debugDescription : msg) + } + } + } +} + +// MARK: - 内部方法 +extension APLogin2FAVC { + + func closeView() { + guard let window = view.window, let parent = window.sheetParent + else { return } + parent.endSheet(window) + } + + func showTips(_ text: String) { + if text.isEmpty { + tipsWarningView.isHidden = true + tipsWarningView.stringValue = "" + } else { + tipsWarningView.stringValue = text + tipsWarningView.isHidden = false + } + } + + func viewEnabled(_ isEnabled: Bool) { + showTips("") + verifyBtn.isEnabled = isEnabled + isEnabled ? indicatorView.stopAnimation(nil) : indicatorView.startAnimation(nil) + } + + func verifyCodeCountdown() { + self.lastTime = 30 + self.sendCodeBtn.title = "\(self.lastTime)s 后重试" + self.sendCodeBtn.isEnabled = false + self.verifyCodeTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.verifyCodeTime), userInfo: nil, repeats: true) + } + + // 验证码倒计时 + @objc func verifyCodeTime() { + lastTime -= 1 + sendCodeBtn.title = "\(self.lastTime)s 后重试" + if lastTime <= 0 { + sendCodeBtn.title = "重新发送验证码" + sendCodeBtn.isEnabled = true + verifyCodeTimer?.invalidate() + } + } +} + +// MARK: - NSTextFieldDelegate +extension APLogin2FAVC: NSTextFieldDelegate { + + func controlTextDidChange(_ obj: Notification) { + if phoneCodeView.stringValue.count == 6 { + verifyBtn.isEnabled = true + } else { + verifyBtn.isEnabled = false + } + } +} diff --git a/AppleParty/AppleParty/LoginView/APLogin2FAVC.xib b/AppleParty/AppleParty/LoginView/APLogin2FAVC.xib new file mode 100644 index 0000000..46822c6 --- /dev/null +++ b/AppleParty/AppleParty/LoginView/APLogin2FAVC.xib @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/LoginView/APLoginVC.swift b/AppleParty/AppleParty/LoginView/APLoginVC.swift new file mode 100644 index 0000000..b0c6267 --- /dev/null +++ b/AppleParty/AppleParty/LoginView/APLoginVC.swift @@ -0,0 +1,234 @@ +// +// APLoginVC.swift +// AppleParty +// +// Created by HTC on 2022/3/17. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APLoginVC: NSViewController { + + public var cancelHandle: (() -> Void)? + public var successHandle: (() -> Void)? + + @IBOutlet weak var accountView: NSTextField! + @IBOutlet weak var passwordView: NSSecureTextField! + // 历史账号 + @IBOutlet weak var historyBox: NSBox! + @IBOutlet weak var tableView: NSTableView! + + @IBOutlet weak var tipsWarningView: NSTextField! + @IBOutlet weak var autoLoginBtn: NSButton! + @IBOutlet weak var indicatorView: NSProgressIndicator! + @IBOutlet weak var loginBtn: NSButton! + + override func viewDidLoad() { + super.viewDidLoad() + accountView.delegate = self + passwordView.delegate = self + tipsWarningView.maximumNumberOfLines = 5 + + // 最近登录的账号 + let user = UserCenter.shared.loginedUser + let name = user.appleid + let pwd = user.password + guard name.count > 0, pwd.count > 0 else { return } + accountView.stringValue = name + passwordView.stringValue = pwd + viewEnabled(true) + + // 如果需要自动登录 + if UserCenter.shared.isAutoLogin || UserCenter.shared.isFirstTime { + loginAccount() + UserCenter.shared.isFirstTime = false + } + } + + @IBAction func clickedCancelBtn(_ sender: NSButton) { + closeView() + cancelHandle?() + } + + + @IBAction func showAccountHistoryList(_ sender: Any) { + if historyBox.isHidden { + tableView.delegate = self + tableView.dataSource = self + tableView.reloadData() + } + historyBox.isHidden = !historyBox.isHidden + } + + @IBAction func clickedLoginBtn(_ sender: NSButton) { + loginAccount() + } +} + + +// MARK: - 网络请求 +extension APLoginVC { + + func loginAccount() { + if accountView.stringValue.isEmpty { + showTips("苹果账号不能为空!") + return + } + if passwordView.stringValue.isEmpty { + showTips("密码不能为空!") + return + } + + viewEnabled(false) + + let account = accountView.stringValue + let pwd = passwordView.stringValue + + APClient.signIn(account: account, password: pwd).request { [weak self] result, response, error in + self?.viewEnabled(true) + if let err = error, let type = APClientErrorCode(rawValue: err.code) { + switch type { + case .notAuthorized: + self?.showTips("Apple ID 或密码不正确") + case .twoStepOrFactor: + // 保存账号密码 + if self?.autoLoginBtn.state == .on { + UserCenter.shared.isAutoLogin = true + UserCenter.shared.loginedUser = User(appleid: account, password: pwd) + } + // 双重认证 + let vc = APLogin2FAVC() + vc.cancelHandle = { [weak self] in + self?.viewEnabled(true) + } + vc.successHandle = { [weak self] in + self?.trusDevice() + self?.successHandle?() + self?.closeView() + } + let pannel = NSPanel(contentViewController: vc) + pannel.setFrame(NSRect(origin: .zero, size: NSSize(width: 500, height: 360)), display: true) + self?.view.window?.beginSheet(pannel, completionHandler: nil) + default: + self?.showTips(err.localizedDescription) + } + return + } + let code = response?.statusCode + // 登陆态有效 + if code == 200 { + // 保存账号密码 + if self?.autoLoginBtn.state == .on { + UserCenter.shared.isAutoLogin = true + UserCenter.shared.loginedUser = User(appleid: account, password: pwd) + } + self?.validateSession() + } else { + self?.showTips("\(code ?? 0),\(error.debugDescription)") + } + } + } + + func validateSession() { + viewEnabled(false) + APClient.signInSession.request { [weak self] result, response, error in + self?.viewEnabled(true) + let code = response?.statusCode + switch code { + case 200, 201: + UserCenter.shared.isAuthorized = true + self?.trusDevice() + self?.successHandle?() + self?.closeView() + default: + let errors = dictionaryArray(result["serviceErrors"]) + let msg = string(from: errors.first?["message"]) + self?.showTips(msg.isEmpty ? error.debugDescription : msg) + } + } + } + + func trusDevice() { + guard InfoCenter.shared.trusDevice else { return} + + APClient.trusDevice(isTrus: true).request { result, response, error in + if response?.statusCode == 204 { + debugPrint("信任设备成功~") + } + } + } + +} + + +// MARK: - 内部方法 +extension APLoginVC { + + func showTips(_ text: String) { + if text.isEmpty { + tipsWarningView.isHidden = true + tipsWarningView.stringValue = "" + } else { + tipsWarningView.stringValue = text + tipsWarningView.isHidden = false + } + } + + func viewEnabled(_ isEnabled: Bool) { + showTips("") + loginBtn.isEnabled = isEnabled + historyBox.isHidden = true + isEnabled ? indicatorView.stopAnimation(nil) : indicatorView.startAnimation(nil) + } + + func closeView() { + guard let window = view.window, let parent = window.sheetParent + else { return } + parent.endSheet(window) + } + +} + +// MARK: - NSTextFieldDelegate +extension APLoginVC: NSTextFieldDelegate { + + func controlTextDidChange(_ obj: Notification) { + if !accountView.stringValue.isEmpty && !passwordView.stringValue.isEmpty { + loginBtn.isEnabled = true + } else { + loginBtn.isEnabled = false + showTips("") + } + } +} + +// MARK: - NSTableViewDelegate +extension APLoginVC: NSTableViewDelegate, NSTableViewDataSource { + + func numberOfRows(in tableView: NSTableView) -> Int { + return UserCenter.shared.historyUser.count + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + if tableColumn?.identifier == NSUserInterfaceItemIdentifier(rawValue: "nameColumn") { + let cellIdentifier = NSUserInterfaceItemIdentifier(rawValue: "nameCell") + guard let cellView = tableView.makeView(withIdentifier: cellIdentifier, owner: self) as? NSTableCellView else { return nil } + cellView.textField?.stringValue = UserCenter.shared.historyUser[row].appleid + return cellView + } + return nil + } + + func tableViewSelectionDidChange(_ notification: Notification){ + let tableView = notification.object as! NSTableView + let clickedRow = tableView.selectedRow + guard clickedRow >= 0 else { + return + } + tableView.deselectRow(clickedRow) + accountView.stringValue = UserCenter.shared.historyUser[clickedRow].appleid + passwordView.stringValue = UserCenter.shared.historyUser[clickedRow].password + showAccountHistoryList(clickedRow) + } +} diff --git a/AppleParty/AppleParty/LoginView/APLoginVC.xib b/AppleParty/AppleParty/LoginView/APLoginVC.xib new file mode 100644 index 0000000..0fb725b --- /dev/null +++ b/AppleParty/AppleParty/LoginView/APLoginVC.xib @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/LoginView/APWebLoginVC.swift b/AppleParty/AppleParty/LoginView/APWebLoginVC.swift new file mode 100644 index 0000000..4467228 --- /dev/null +++ b/AppleParty/AppleParty/LoginView/APWebLoginVC.swift @@ -0,0 +1,137 @@ +// +// APWebLoginVC.swift +// AppleParty +// +// Created by HTC on 2024/10/29. +// Copyright © 2024 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APWebLoginVC: NSViewController { + + public var cancelHandle: (() -> Void)? + public var successHandle: (() -> Void)? + private var webCore: AppleWebLoginCore? = nil + + @IBOutlet weak var loginBtn: NSButton! + @IBOutlet weak var cancelBtn: NSButton! + @IBOutlet weak var indicatorView: NSProgressIndicator! + @IBOutlet weak var tipsWarningView: NSTextField! + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + } + + @IBAction func clickedCancelBtn(_ sender: NSButton) { + closeView() + cancelHandle?() + } + + @IBAction func clickedLoginBtn(_ sender: NSButton) { + validateSession() + } + + // 判断是登陆态是否过期 + func validateSession() { + viewEnabled(false) + APClient.signInSession.request { [weak self] result, response, error in + self?.viewEnabled(true) + let code = response?.statusCode + switch code { + case 200, 201: + UserCenter.shared.isAuthorized = true + self?.successHandle?() + self?.closeView() + default: + let errors = dictionaryArray(result["serviceErrors"]) + let msg = string(from: errors.first?["message"]) + self?.showTips(msg.isEmpty ? error.debugDescription : msg) + // 隐藏按钮透视显示 + self?.cancelBtn.isEnabled = false + self?.loginBtn.isEnabled = false + self?.loginWithWeb() + } + } + } + + func loginWithWeb() { + let appleWebLoginCore = AppleWebLoginCore() + // 将 webView 添加到视图层次结构中 + self.view.addSubview(appleWebLoginCore.webView) + + let closeButton = NSButton(title: "取消", target: self, action: #selector(closeButtonClicked)) + closeButton.attributedTitle = NSAttributedString(string: "取消", attributes: [NSAttributedString.Key.foregroundColor: NSColor.gray]) + closeButton.keyEquivalent = "\u{1B}" // `esc` 快捷键 + appleWebLoginCore.webView.addSubview(closeButton) + + // 设置 webView 的约束以适应视图 + appleWebLoginCore.webView.translatesAutoresizingMaskIntoConstraints = false + closeButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + appleWebLoginCore.webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), + appleWebLoginCore.webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), + appleWebLoginCore.webView.topAnchor.constraint(equalTo: self.view.topAnchor), + appleWebLoginCore.webView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor), + + closeButton.trailingAnchor.constraint(equalTo: appleWebLoginCore.webView.trailingAnchor, constant: -10), + closeButton.topAnchor.constraint(equalTo: appleWebLoginCore.webView.topAnchor, constant: 10) + ]) + + // 关闭按钮 + + + appleWebLoginCore.installFirstLoadCompleteTrap { + // 处理首次加载完成的逻辑 + print("First load complete") + } + + appleWebLoginCore.installCredentialPopulationTrap { token, cookies in + // 处理凭据填充的逻辑 + print("Received cookies: \(cookies)") + print("Received token: \(token)") + + if let cks = APClientSession.shared.config.httpCookieStorage?.cookies { + for ck in cks { + APClientSession.shared.config.httpCookieStorage?.deleteCookie(ck) + } + } + for cookie in cookies { + APClientSession.shared.config.httpCookieStorage?.setCookie(cookie) + } +// APClientSession.shared.config.headers.update(name: "Cookie", value: "myacinfo=\(token);") + self.validateSession() + } + self.webCore = appleWebLoginCore + } + + @objc func closeButtonClicked() { + // 处理关闭按钮的点击事件 + print("关闭按钮被点击") + closeView() + } + + func viewEnabled(_ isEnabled: Bool) { + showTips("") + loginBtn.isEnabled = isEnabled + isEnabled ? indicatorView.stopAnimation(nil) : indicatorView.startAnimation(nil) + } + + func showTips(_ text: String) { + if text.isEmpty { + tipsWarningView.isHidden = true + tipsWarningView.stringValue = "" + } else { + tipsWarningView.stringValue = text + tipsWarningView.isHidden = false + } + } + + func closeView() { + guard let window = view.window, let parent = window.sheetParent + else { return } + parent.endSheet(window) + } + +} diff --git a/AppleParty/AppleParty/LoginView/APWebLoginVC.xib b/AppleParty/AppleParty/LoginView/APWebLoginVC.xib new file mode 100644 index 0000000..cade6f3 --- /dev/null +++ b/AppleParty/AppleParty/LoginView/APWebLoginVC.xib @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 提示: +登录通过 https://appstoreconnect.apple.com 网页进行授权 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/LoginView/AppleWebLogin/AppleWebLoginCore.swift b/AppleParty/AppleParty/LoginView/AppleWebLogin/AppleWebLoginCore.swift new file mode 100644 index 0000000..42cf296 --- /dev/null +++ b/AppleParty/AppleParty/LoginView/AppleWebLogin/AppleWebLoginCore.swift @@ -0,0 +1,127 @@ +// +// AppleWebLoginCore.swift +// AppleWebLogin +// +// Created by 秋星桥 on 2024/10/23. +// ref: https://github.com/Lakr233/AppleWebLogin + +import Combine +@preconcurrency import WebKit + +//private let loginURL = URL(string: "https://account.apple.com/sign-in")! +private let loginURL = URL(string: "https://appstoreconnect.apple.com/login")! + +public class AppleWebLoginCore: NSObject, WKUIDelegate, WKNavigationDelegate { + var webView: WKWebView { + associatedWebView + } + + private let associatedWebView: WKWebView + private var dataPopulationTimer: Timer? = nil + private var firstLoadComplete = false + + public private(set) var onFirstLoadComplete: (() -> Void)? + public var onCredentialPopulation: ((String, [HTTPCookie]) -> Void)? + + override public init() { + let contentController = WKUserContentController() + let configuration = WKWebViewConfiguration() + configuration.defaultWebpagePreferences.allowsContentJavaScript = true + configuration.userContentController = contentController + configuration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs") + configuration.websiteDataStore = .nonPersistent() + + associatedWebView = .init( + frame: CGRect(x: 0, y: 0, width: 1920, height: 1080), + configuration: configuration + ) + associatedWebView.isHidden = true + + super.init() + + associatedWebView.uiDelegate = self + associatedWebView.navigationDelegate = self + + associatedWebView.load(.init(url: loginURL)) + + #if DEBUG + if associatedWebView.responds(to: Selector(("setInspectable:"))) { + associatedWebView.perform(Selector(("setInspectable:")), with: true) + } + #endif + + let dataPopulationTimer = Timer(timeInterval: 1, repeats: true) { [weak self] _ in + guard let self else { return } + removeUnwantedElements() + populateData() + } + RunLoop.main.add(dataPopulationTimer, forMode: .common) + self.dataPopulationTimer = dataPopulationTimer + } + + deinit { + dataPopulationTimer?.invalidate() + onCredentialPopulation = nil + } + + public func webView(_: WKWebView, didFinish _: WKNavigation!) { + guard !firstLoadComplete else { return } + defer { firstLoadComplete = true } + associatedWebView.isHidden = false + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.onFirstLoadComplete?() + self.onFirstLoadComplete = nil + } + } + +// public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { +// let request = navigationAction.request +// if let headers = request.allHTTPHeaderFields { +// print(request.url?.absoluteString) +// print("headers: \(headers)") +// if let scntValue = headers["scnt"] { +// print("scnt value: \(scntValue)") +// } +// } +// decisionHandler(.allow, preferences) +// } + + public func installFirstLoadCompleteTrap(_ block: @escaping () -> Void) { + onFirstLoadComplete = block + } + + public func installCredentialPopulationTrap(_ block: @escaping (String, [HTTPCookie]) -> Void) { + onCredentialPopulation = block + } + + private func removeUnwantedElements() { + let removeElements = """ + Element.prototype.remove = function() { + this.parentElement.removeChild(this); + } + NodeList.prototype.remove = HTMLCollection.prototype.remove = function() { + for(var i = this.length - 1; i >= 0; i--) { + if(this[i] && this[i].parentElement) { + this[i].parentElement.removeChild(this[i]); + } + } + } + document.getElementById("header").remove(); + document.getElementsByClassName('landing__animation').remove(); + """ + associatedWebView.evaluateJavaScript(removeElements) { _, _ in + } + } + + private func populateData() { + guard let onCredentialPopulation else { return } + associatedWebView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in + //print(cookies) + for cookie in cookies where cookie.name == "myacinfo" { + let value = cookie.value + onCredentialPopulation(value, cookies) + self.onCredentialPopulation = nil + } + } + } +} diff --git a/AppleParty/AppleParty/LoginView/PhoneNumbers.swift b/AppleParty/AppleParty/LoginView/PhoneNumbers.swift new file mode 100644 index 0000000..bb07e0a --- /dev/null +++ b/AppleParty/AppleParty/LoginView/PhoneNumbers.swift @@ -0,0 +1,27 @@ +// +// PhoneNumbers.swift +// AppleParty +// +// Created by 易承 on 2021/6/2. +// + +import Foundation + +struct PNumber { + var num: String + var id: Int +} + +// MARK: - 双重绑定手机号码 +struct PhoneNumbers { + var numbers: [PNumber] + + init(body: [String: Any]) { + numbers = [PNumber]() + let trustedPhoneNumbers = dictionaryArray(body["trustedPhoneNumbers"]) + for phone in trustedPhoneNumbers { + numbers.append(PNumber(num: string(from: phone["numberWithDialCode"], defaultValue: "未知手机号"), + id: int(from: phone["id"]) ?? 0 )) + } + } +} diff --git a/AppleParty/AppleParty/QRcodeView/APQRcode.storyboard b/AppleParty/AppleParty/QRcodeView/APQRcode.storyboard new file mode 100644 index 0000000..b042bb2 --- /dev/null +++ b/AppleParty/AppleParty/QRcodeView/APQRcode.storyboard @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/QRcodeView/APQRcodeVC.swift b/AppleParty/AppleParty/QRcodeView/APQRcodeVC.swift new file mode 100644 index 0000000..e3e5fe0 --- /dev/null +++ b/AppleParty/AppleParty/QRcodeView/APQRcodeVC.swift @@ -0,0 +1,199 @@ +// +// APQRcodeVC.swift +// AppleParty +// +// Created by HTC on 2022/3/24. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APQRcodeVC: NSViewController { + + @IBOutlet weak var inputTextField: NSTextField! + @IBOutlet weak var qrcodeSizeBtn: NSPopUpButton! + @IBOutlet weak var createQrcodeBtn: NSButton! + @IBOutlet weak var qrcodeImageView: NSImageView! + @IBOutlet weak var copyQrcodeBtn: NSButton! + @IBOutlet weak var saveQrcodeBtn: NSButton! + @IBOutlet weak var shareQrcodeBtn: NSButton! + @IBOutlet weak var shareQrcodeByAirDropBtn: NSButton! + @IBOutlet weak var scanQrcodeBtn: NSButton! + @IBOutlet weak var messageLbl: NSTextField! + @IBOutlet weak var textScrollView: NSScrollView! + @IBOutlet weak var textView: NSTextView! + + @IBAction func createQrcode(_ sender: Any) { + let str = inputTextField!.stringValue + if str.isEmpty { + enableQrcode(false) + statusMessage("") + NSAlert.show("请输出需要生成二维码的文本!") + return + } + + let img = createQRImage(str, NSMakeSize(360, 360)) + qrcodeImageView.image = img + enableQrcode(true) + statusMessage("二维码生成成功") + } + + @IBAction func copyQrcode(_ sender: Any) { + let str = inputTextField!.stringValue + if !str.isEmpty { + let img = createQRImage(str, getImageSize()) + let pb = NSPasteboard.general + pb.clearContents() + if pb.writeObjects([img as NSPasteboardWriting]) { + statusMessage("Copy QRCode to clipboard") + } else { + statusMessage("Failed to copy QRCode to clipboard") + } + } + } + + + @IBAction func saveQrcode(_ sender: Any) { + let str = inputTextField!.stringValue + if str.isEmpty { + return statusMessage("请填写有效的文本内容!") + } + + let savePanel = NSSavePanel() + savePanel.title = "Save QRCode As File" + savePanel.canCreateDirectories = true + savePanel.allowedFileTypes = ["png"] + savePanel.isExtensionHidden = false + savePanel.nameFieldStringValue = getImgaeName() + ".png" + savePanel.becomeKey() + let result = savePanel.runModal() + if (result == .OK && (savePanel.url) != nil) { + let img = createQRImage(str, getImageSize()) + let imgRep = NSBitmapImageRep(data: img.tiffRepresentation!) + let data = imgRep?.representation(using: NSBitmapImageRep.FileType.png, properties: [:]) + try! data?.write(to: savePanel.url!) + statusMessage("Save QRCode to \(savePanel.url!.absoluteString)") + } + } + + @IBAction func shareQrcode(_ sender: Any) { + let str = inputTextField!.stringValue + if str.isEmpty { + return statusMessage("请填写有效的文本内容!") + } + + let img = createQRImage(str, getImageSize()) + let picker = NSSharingServicePicker(items: [img]) + picker.delegate = self + picker.show(relativeTo: .zero, of: sender as! NSView, preferredEdge: .maxX) + } + + @IBAction func shareQrcodeByAirDrop(_ sender: Any) { + let str = inputTextField!.stringValue + if str.isEmpty { + return statusMessage("请填写有效的文本内容!") + } + + let img = createQRImage(str, getImageSize()) + let service = NSSharingService(named: .sendViaAirDrop)! + let items: [NSImage] = [img] + if service.canPerform(withItems: items) { + service.delegate = self + service.perform(withItems: items) + } else { + statusMessage("Cannot perform AirDrop!") + } + } + + @IBAction func scanQrcode(_ sender: Any) { + enableQrcode(false) + qrcodeImageView.isHidden = true + textView.string = "" + textScrollView.isHidden = false + // scan QRCode + let dict = scanQRCodeOnScreen() as! [String:Any] + let data = dict["qrcode"] as! Array + if data.isEmpty { + textView.string = "Not found valid QRCode of screen!" + return + } + // output message + appendToTextView("识别到二维码个数:\(data.count)\n", coreText: "\(data.count)") + for (index, element) in data.enumerated() { + let k = index + 1 + appendToTextView("\n第\(k)个二维码内容:\n【\n\(element)\n】\n", coreText: element) + } + } + + override func viewDidAppear() { + super.viewDidAppear() + inputTextField.becomeFirstResponder() + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + func setupUI() { + inputTextField.delegate = self + enableQrcode(false) + qrcodeImageView.image = NSImage(named: "QRcode") + } + + func enableQrcode(_ enable: Bool) { + copyQrcodeBtn.isEnabled = enable + saveQrcodeBtn.isEnabled = enable + shareQrcodeBtn.isEnabled = enable + shareQrcodeByAirDropBtn.isEnabled = enable + textScrollView.isHidden = true + qrcodeImageView.isHidden = false + if !enable { + statusMessage("") + qrcodeImageView.image = NSImage(named: "QRcode") + } + } + + func statusMessage(_ msg: String) { + messageLbl.stringValue = msg.count == 0 ? "" : "提示:\(msg)" + } + + func getImageSize() -> NSSize { + let wh = CGFloat(qrcodeSizeBtn?.selectedTag() ?? 500) + let size = NSMakeSize(wh, wh) + return size + } + + func getImgaeName() -> String { + let dateFormatter : DateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH-mm-ss" + let date = Date() + let dateString = dateFormatter.string(from: date) + return "AppleParty_qrcode-" + dateString + } + + func appendToTextView(_ text: String, coreText: String) { + let attributes = [NSAttributedString.Key.foregroundColor: NSColor.labelColor] + let secondAttributes = [NSAttributedString.Key.foregroundColor: #colorLiteral(red: 0.3211918473, green: 0.7199308276, blue: 1, alpha: 1)] + let attr = NSMutableAttributedString.init(string: text, attributes: attributes) + attr.addAttributes(secondAttributes, range: (text as NSString).range(of: coreText)) + textView.textStorage?.append(attr) + textView.scrollRangeToVisible(NSMakeRange(textView.string.count, 0)) + } +} + + +extension APQRcodeVC: NSTextFieldDelegate { + func controlTextDidChange(_ obj: Notification) { + let textField = obj.object as! NSTextField + if textField.stringValue.isEmpty { + enableQrcode(false) + } + } +} + + +extension APQRcodeVC: NSSharingServicePickerDelegate, NSSharingServiceDelegate { + + +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37M-logo.imageset/37M-logo.png b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37M-logo.imageset/37M-logo.png new file mode 100644 index 0000000..0f98464 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37M-logo.imageset/37M-logo.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37M-logo.imageset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37M-logo.imageset/Contents.json new file mode 100644 index 0000000..981e871 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37M-logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "37M-logo.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37M-slogan.imageset/37M-slogan.png b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37M-slogan.imageset/37M-slogan.png new file mode 100644 index 0000000..75bbcf4 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37M-slogan.imageset/37M-slogan.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37M-slogan.imageset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37M-slogan.imageset/Contents.json new file mode 100644 index 0000000..ba973e5 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37M-slogan.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "37M-slogan.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37iOSTeam-Round.imageset/37iOSTeam-Round.png b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37iOSTeam-Round.imageset/37iOSTeam-Round.png new file mode 100644 index 0000000..32a732b Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37iOSTeam-Round.imageset/37iOSTeam-Round.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37iOSTeam-Round.imageset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37iOSTeam-Round.imageset/Contents.json new file mode 100644 index 0000000..2b903f6 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/37iOSTeam-Round.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "37iOSTeam-Round.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/37MobileGames/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..786e67f --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "mac", + "size" : "16x16", + "filename" : "icon-16.png", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "16x16", + "filename" : "icon-32.png", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "filename" : "icon-32.png", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "filename" : "icon-64.png", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "filename" : "icon-128.png", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "filename" : "icon-256.png", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "filename" : "icon-256.png", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "filename" : "icon-512.png", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "filename" : "icon-512.png", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "filename" : "icon-1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } + } diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-1024.png b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-1024.png new file mode 100644 index 0000000..296f8d1 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-1024.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-128.png b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-128.png new file mode 100644 index 0000000..fb2cfb4 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-128.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-16.png b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-16.png new file mode 100644 index 0000000..df68e05 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-16.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-256.png b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-256.png new file mode 100644 index 0000000..7de8ebe Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-256.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-32.png b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-32.png new file mode 100644 index 0000000..03e7f02 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-32.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-512.png b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-512.png new file mode 100644 index 0000000..f1e4fcd Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-512.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-64.png b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-64.png new file mode 100644 index 0000000..ff72e39 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/AppIcon.appiconset/icon-64.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/ApplePartyIcon.imageset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/ApplePartyIcon.imageset/Contents.json new file mode 100644 index 0000000..859ebcc --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/ApplePartyIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon-256.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon-512.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/ApplePartyIcon.imageset/icon-256.png b/AppleParty/AppleParty/Resources/Assets.xcassets/ApplePartyIcon.imageset/icon-256.png new file mode 100644 index 0000000..7de8ebe Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/ApplePartyIcon.imageset/icon-256.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/ApplePartyIcon.imageset/icon-512.png b/AppleParty/AppleParty/Resources/Assets.xcassets/ApplePartyIcon.imageset/icon-512.png new file mode 100644 index 0000000..f1e4fcd Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/ApplePartyIcon.imageset/icon-512.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/AppAnalytics.imageset/AppAnalytics@2x.png b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/AppAnalytics.imageset/AppAnalytics@2x.png new file mode 100644 index 0000000..355de51 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/AppAnalytics.imageset/AppAnalytics@2x.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/AppAnalytics.imageset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/AppAnalytics.imageset/Contents.json new file mode 100644 index 0000000..de23fe8 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/AppAnalytics.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "AppAnalytics@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/Apps.imageset/Apps@2x.png b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/Apps.imageset/Apps@2x.png new file mode 100644 index 0000000..720aeb6 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/Apps.imageset/Apps@2x.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/Apps.imageset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/Apps.imageset/Contents.json new file mode 100644 index 0000000..2cf0691 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/Apps.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Apps@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/FinancialReports.imageset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/FinancialReports.imageset/Contents.json new file mode 100644 index 0000000..35dcdf4 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/FinancialReports.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "FinancialReports@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/FinancialReports.imageset/FinancialReports@2x.png b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/FinancialReports.imageset/FinancialReports@2x.png new file mode 100644 index 0000000..cf0ad51 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/FinancialReports.imageset/FinancialReports@2x.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/IPAUpload.imageset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/IPAUpload.imageset/Contents.json new file mode 100644 index 0000000..62e9020 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/IPAUpload.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "IPAUpload@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/IPAUpload.imageset/IPAUpload@2x.png b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/IPAUpload.imageset/IPAUpload@2x.png new file mode 100644 index 0000000..008b16a Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/IPAUpload.imageset/IPAUpload@2x.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/PlaceholderIcon.imageset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/PlaceholderIcon.imageset/Contents.json new file mode 100644 index 0000000..e44b606 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/PlaceholderIcon.imageset/Contents.json @@ -0,0 +1,52 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "PlaceholderIcon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "PlaceholderIcon_white@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/PlaceholderIcon.imageset/PlaceholderIcon@2x.png b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/PlaceholderIcon.imageset/PlaceholderIcon@2x.png new file mode 100644 index 0000000..1fa87a4 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/PlaceholderIcon.imageset/PlaceholderIcon@2x.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/PlaceholderIcon.imageset/PlaceholderIcon_white@2x.png b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/PlaceholderIcon.imageset/PlaceholderIcon_white@2x.png new file mode 100644 index 0000000..de3a2c0 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/PlaceholderIcon.imageset/PlaceholderIcon_white@2x.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/QRcode.imageset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/QRcode.imageset/Contents.json new file mode 100644 index 0000000..532c014 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/QRcode.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "QRcode@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/QRcode.imageset/QRcode@2x.png b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/QRcode.imageset/QRcode@2x.png new file mode 100644 index 0000000..077ac69 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/QRcode.imageset/QRcode@2x.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/SendEmail.imageset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/SendEmail.imageset/Contents.json new file mode 100644 index 0000000..8754c55 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/SendEmail.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "SendEmail@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/SendEmail.imageset/SendEmail@2x.png b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/SendEmail.imageset/SendEmail@2x.png new file mode 100644 index 0000000..ada5696 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/SendEmail.imageset/SendEmail@2x.png differ diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/VerifyReceipt.imageset/Contents.json b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/VerifyReceipt.imageset/Contents.json new file mode 100644 index 0000000..b22decb --- /dev/null +++ b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/VerifyReceipt.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "VerifyReceipt@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/VerifyReceipt.imageset/VerifyReceipt@2x.png b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/VerifyReceipt.imageset/VerifyReceipt@2x.png new file mode 100644 index 0000000..1805cb4 Binary files /dev/null and b/AppleParty/AppleParty/Resources/Assets.xcassets/RootView/VerifyReceipt.imageset/VerifyReceipt@2x.png differ diff --git a/AppleParty/AppleParty/Resources/InAppPurchase/example.xlsx b/AppleParty/AppleParty/Resources/InAppPurchase/example.xlsx new file mode 100644 index 0000000..dc6cc75 Binary files /dev/null and b/AppleParty/AppleParty/Resources/InAppPurchase/example.xlsx differ diff --git a/AppleParty/AppleParty/Resources/Transporter/iap_metadata.xml b/AppleParty/AppleParty/Resources/Transporter/iap_metadata.xml new file mode 100644 index 0000000..328bbcf --- /dev/null +++ b/AppleParty/AppleParty/Resources/Transporter/iap_metadata.xml @@ -0,0 +1,119 @@ + + + {team_id} + + {SKU} + + + + + com.app.1usd + 1ud(2~64 个字符) + + consumable + + + true + 3 + + + + + 2~30个字符 + 至少10~45个字符 + + + 中文2~30个字符 + 至少10~45个字符 + + + + + 636132 + IMG_5180.PNG + xxxxx + + Some notes for the reviewer.(2~4000字符) + + + + com.app.3usd + 3ud(2~64 个字符) + non-consumable + + + true + 3 + + + + + 2~30个字符 + 至少10~45个字符 + + + + + 636132 + IMG_5180.PNG + xxxxx + + Some notes for the reviewer.(2~4000字符) + + + + + 订阅群组显示名称(2-75) + App 名称显示选项:使用自定义名称(2-30) + + + + every_movie_in_the_world_plus_1month + 参考名称 + auto-renewable + 1 Month + 7 Days + true + + + 订阅显示名称 + 订阅描述Every movie ever plus. + + + + 636132 + IMG_5180.PNG + xxxxx + + Some notes for the reviewer.(2~4000字符) + + + + + com.app.6usd + 6ud(2~64 个字符) + subscription + + + true + 49 + + + + + 2~30个字符 + 至少10~45个字符 + + + + + 636132 + IMG_5180.PNG + xxxxx + + Some notes for the reviewer.(2~4000字符) + + + + + diff --git a/AppleParty/AppleParty/Resources/Transporter/ipa_metadata.xml b/AppleParty/AppleParty/Resources/Transporter/ipa_metadata.xml new file mode 100644 index 0000000..69ae5b1 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Transporter/ipa_metadata.xml @@ -0,0 +1,12 @@ + + + + + + {file_size} + {file_name} + {file_md5} + + + + diff --git a/AppleParty/AppleParty/Resources/Transporter/shot_metadata.xml b/AppleParty/AppleParty/Resources/Transporter/shot_metadata.xml new file mode 100644 index 0000000..55fffc0 --- /dev/null +++ b/AppleParty/AppleParty/Resources/Transporter/shot_metadata.xml @@ -0,0 +1,36 @@ + + + {provider} + {provider} + + {vendor_id} + + + + + + {title} + + + + {video_size} + {video_name} + {video_md5} + + {00:00:05:00} + + + + + {image_size} + {image_name} + {image_md5} + + + + + + + + + diff --git a/AppleParty/AppleParty/RootView/APRootCollectionAdapter.swift b/AppleParty/AppleParty/RootView/APRootCollectionAdapter.swift new file mode 100644 index 0000000..ad48f8f --- /dev/null +++ b/AppleParty/AppleParty/RootView/APRootCollectionAdapter.swift @@ -0,0 +1,61 @@ +// +// APRootCollectionAdapter.swift +// AppleParty +// +// Created by HTC on 2022/3/14. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APRootCollectionAdapter: NSObject { + fileprivate static let numberOfSections = 1 + fileprivate static let itemId = "APRootCollectionCell" + + fileprivate var items = [APRootCollectionModel]() { + didSet { + collectionView.reloadData() + } + } + private var collectionView: NSCollectionView + + init(collectionView: NSCollectionView) { + self.collectionView = collectionView + super.init() + self.collectionView.dataSource = self + self.collectionView.delegate = self + self.collectionView.register(APRootCollectionCell.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: APRootCollectionAdapter.itemId)) + } + + func set(items: [APRootCollectionModel]) { + self.items = items + } +} + + +extension APRootCollectionAdapter: NSCollectionViewDataSource, NSCollectionViewDelegate { + func numberOfSectionsInCollectionView(collectionView: NSCollectionView) -> Int { + return APRootCollectionAdapter.numberOfSections + } + + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + return items.count + } + + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { + let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: APRootCollectionAdapter.itemId), for: indexPath) + guard let collectionViewItem = item as? APRootCollectionCell else { return item } + + let name = items[indexPath.item].name + let icon = items[indexPath.item].icon + collectionViewItem.configure(name: name, icon: icon) + return item + } + + func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { + collectionView.deselectItems(at: indexPaths) + if let item = indexPaths.first?.item, let handler = items[item].handler { + handler() + } + } +} diff --git a/AppleParty/AppleParty/RootView/APRootCollectionCell.swift b/AppleParty/AppleParty/RootView/APRootCollectionCell.swift new file mode 100644 index 0000000..9b9b651 --- /dev/null +++ b/AppleParty/AppleParty/RootView/APRootCollectionCell.swift @@ -0,0 +1,25 @@ +// +// APRootCollectionCell.swift +// AppleParty +// +// Created by HTC on 2022/3/14. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APRootCollectionCell: NSCollectionViewItem { + + @IBOutlet weak var imgView: NSImageView! + @IBOutlet weak var nameView: NSTextField! + + override func viewDidLoad() { + super.viewDidLoad() + } + + func configure(name: String, icon: String) { + nameView.stringValue = name + imgView?.image = NSImage(named: icon) + } + +} diff --git a/AppleParty/AppleParty/RootView/APRootCollectionCell.xib b/AppleParty/AppleParty/RootView/APRootCollectionCell.xib new file mode 100644 index 0000000..e1ade3f --- /dev/null +++ b/AppleParty/AppleParty/RootView/APRootCollectionCell.xib @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/RootView/APRootCollectionModel.swift b/AppleParty/AppleParty/RootView/APRootCollectionModel.swift new file mode 100644 index 0000000..640f45c --- /dev/null +++ b/AppleParty/AppleParty/RootView/APRootCollectionModel.swift @@ -0,0 +1,17 @@ +// +// APRootCollectionModel.swift +// AppleParty +// +// Created by HTC on 2022/3/14. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + + +struct APRootCollectionModel { + let name: String + let icon: String + let handler: (() -> Void)? + +} diff --git a/AppleParty/AppleParty/RootView/APRootVC.swift b/AppleParty/AppleParty/RootView/APRootVC.swift new file mode 100644 index 0000000..e3fa5ca --- /dev/null +++ b/AppleParty/AppleParty/RootView/APRootVC.swift @@ -0,0 +1,59 @@ +// +// APRootVC.swift +// AppleParty +// +// Created by HTC on 2022/3/11. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APRootVC: NSViewController { + + fileprivate var adapter: APRootCollectionAdapter? + + override func viewDidLoad() { + super.viewDidLoad() + + configureCollectionView() + } + + override func viewDidAppear() { + super.viewDidAppear() + + if UserCenter.shared.isAutoLogin { + let wc = self.view.window?.windowController as! APRootWC + wc.clickedAccountItem(nil) + } + } + + /// 配置显示的功能列表 + func configureCollectionView() { + let colview = APCollectionView() + colview.configure(superView: view) + + adapter = APRootCollectionAdapter(collectionView: colview.collectionView) + let handler = { (isShow: Bool, name: String) in + if isShow { + let mainStoryBoard = NSStoryboard(name: name, bundle: nil) + let windowController = mainStoryBoard.instantiateController(withIdentifier: name) as! NSWindowController + windowController.showWindow(self) + + } else { + APHUD.hide(message: "功能暂未开源,敬请期待~", delayTime: 2) + } + } + + let items = [ + APRootCollectionModel(name: "我的 App", icon: "Apps", handler: { handler(true, "AppList") }), + APRootCollectionModel(name: "App 分析报表", icon: "AppAnalytics", handler: { handler(false, "") }), + APRootCollectionModel(name: "财务报表", icon: "FinancialReports", handler: { handler(false, "") }), + APRootCollectionModel(name: "邮件工具", icon: "SendEmail", handler: { handler(true, "EmailTool") }), + APRootCollectionModel(name: "包体工具", icon: "IPAUpload", handler: { handler(true, "IPAUpload")}), + APRootCollectionModel(name: "二维码工具", icon: "QRcode", handler: { handler(true, "APQRcode")}), + APRootCollectionModel(name: "内购凭证验证", icon: "VerifyReceipt", handler: { handler(true, "APVerifyReceipt")}) + ] + adapter?.set(items: items) + } + +} diff --git a/AppleParty/AppleParty/RootView/APRootWC.swift b/AppleParty/AppleParty/RootView/APRootWC.swift new file mode 100644 index 0000000..4d4a8a6 --- /dev/null +++ b/AppleParty/AppleParty/RootView/APRootWC.swift @@ -0,0 +1,139 @@ +// +// APRootWC.swift +// AppleParty +// +// Created by HTC on 2022/3/11. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APRootWC: NSWindowController { + + override func windowDidLoad() { + super.windowDidLoad() + setupUI() + } + + func setupUI() { + self.window?.title = "AppleParty" + if #available(macOS 11.0, *) { + self.window?.subtitle = "37 Mobile Games" + } + } + + @IBAction func clickedAccountItem(_ sender: NSToolbarItem?) { + // 登陆时,显示切换账号 + if UserCenter.shared.isAuthorized { + if let providers = UserCenter.shared.accountProviders["availableProviders"] as? [[String: Any]] { + var accountList: [Provider] = [] + providers.forEach { provider in + accountList.append(Provider(name: provider["name"] as! String, providerId: String(provider["providerId"] as! Int), publicProviderId: provider["publicProviderId"] as! String)) + } + + accountList.append(Provider(name: "账号登出", providerId: "", publicProviderId: "")) + + let listVC = APSwichAccountPopover() + listVC.accounts = accountList + listVC.selectHandle = { [weak self] row in + guard row >= 0, row < accountList.count else { + return + } + let publicProviderId = accountList[row].publicProviderId + self?.switchAccount(publicProviderId) + } + let pannel = NSPanel(contentViewController: listVC) + pannel.setFrame(NSRect(origin: .zero, size: NSSize(width: 300, height: 350)), display: true) + window?.beginSheet(pannel, completionHandler: nil) + } else { + APHUD.hide(message:"获取登陆账号信息异常~") + } + + } else { + let vc = APWebLoginVC() + vc.successHandle = { [weak self] in + self?.fetchAccountTeamInfo() + self?.window?.title = UserCenter.shared.developerName + if #available(macOS 11.0, *) { + self?.window?.subtitle = UserCenter.shared.accountEmail + } + } + let pannel = NSPanel(contentViewController: vc) + pannel.setFrame(NSRect(origin: .zero, size: NSSize(width: 550, height: 450)), display: true) + window?.beginSheet(pannel, completionHandler: nil) + } + } + + @IBAction func clickedSettingsItem(_ sender: Any) { + let vc = APSettingVC() + let window = NSWindow(contentViewController: vc) + let wc = NSWindowController(window: window) + wc.showWindow(self) + vc.isLoginViewShow = !UserCenter.shared.isAuthorized + } + + @IBAction func clickedGithubItem(_ sender: Any) { + let url = URL(string: kApplePartyGitHub) + NSWorkspace.shared.open(url!) + } + + @IBAction func clicedFeedbackItem(_ sender: Any) { + let url = URL(string: kApplePartyNewIssues) + NSWorkspace.shared.open(url!) + } + + @IBAction func cliced37MobileGamesItem(_ sender: Any) { + let url = URL(string: k37MobileGamesSite) + NSWorkspace.shared.open(url!) + } + + + @IBAction func cliced37iOSTeamItem(_ sender: Any) { + let url = URL(string: k37iOSTeamJueJinSite) + NSWorkspace.shared.open(url!) + } +} + + +extension APRootWC { + + func switchAccount(_ publicProviderId: String) { + guard publicProviderId.count > 0 else { + UserCenter.shared.isAutoLogin = false + UserCenter.shared.isAuthorized = false + // 清掉缓存 + //HTTPCookieStorage.shared.cookies?.forEach(HTTPCookieStorage.shared.deleteCookie) + InfoCenter.shared.cookies = [] + setupUI() + return + } + + APClient.switchProvider(publicProviderId: publicProviderId).request(showLoading: true) { [weak self] result, response, error in + guard let err = error else { + self?.validateSession() + return + } + APHUD.hide(message: err.localizedDescription) + } + } + + func validateSession() { + APClient.signInSession.request(showLoading: true) { [weak self] result, response, error in + guard let err = error else { + UserCenter.shared.isAuthorized = true + self?.window?.title = UserCenter.shared.developerName + if #available(macOS 11.0, *) { + self?.window?.subtitle = UserCenter.shared.accountEmail + } + self?.fetchAccountTeamInfo() + return + } + APHUD.hide(message: err.localizedDescription) + } + } + + func fetchAccountTeamInfo() { + // 获取开发者 Team id 信息 + APClient.ascProvider.request(completionHandler: nil) + } +} diff --git a/AppleParty/AppleParty/RootView/APSwichAccountPopover.swift b/AppleParty/AppleParty/RootView/APSwichAccountPopover.swift new file mode 100644 index 0000000..46bf429 --- /dev/null +++ b/AppleParty/AppleParty/RootView/APSwichAccountPopover.swift @@ -0,0 +1,64 @@ +// +// APSwichAccountPopover.swift +// AppleParty +// +// Created by HTC on 2022/3/18. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APSwichAccountPopover: NSViewController { + + public var accounts = [Provider]() + public var selectHandle: ((_ row: Int) -> Void)? + + @IBOutlet weak var tableView: NSTableView! + + override func viewDidLoad() { + super.viewDidLoad() + tableView.delegate = self + tableView.dataSource = self + } + + @IBAction func cliedCancelBtn(_ sender: Any) { + closeView() + } + + func closeView() { + guard let window = view.window, let parent = window.sheetParent + else { return } + parent.endSheet(window) + } + +} + +extension APSwichAccountPopover: NSTableViewDelegate, NSTableViewDataSource { + + func numberOfRows(in tableView: NSTableView) -> Int { + return accounts.count + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + if tableColumn?.identifier == NSUserInterfaceItemIdentifier(rawValue: "nameColumn") { + let cellIdentifier = NSUserInterfaceItemIdentifier(rawValue: "nameCell") + guard let cellView = tableView.makeView(withIdentifier: cellIdentifier, owner: self) as? NSTableCellView else { return nil } + cellView.textField?.stringValue = accounts[row].name + return cellView + } + return nil + } + + func tableViewSelectionDidChange(_ notification: Notification){ + let tableView = notification.object as! NSTableView + let clickedRow = tableView.selectedRow + guard clickedRow >= 0 else { + return + } + tableView.deselectRow(clickedRow) + if let selectFunc = selectHandle { + selectFunc(clickedRow) + } + closeView() + } +} diff --git a/AppleParty/AppleParty/RootView/APSwichAccountPopover.xib b/AppleParty/AppleParty/RootView/APSwichAccountPopover.xib new file mode 100644 index 0000000..011b16e --- /dev/null +++ b/AppleParty/AppleParty/RootView/APSwichAccountPopover.xib @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/RootView/Base.lproj/Main.storyboard b/AppleParty/AppleParty/RootView/Base.lproj/Main.storyboard new file mode 100644 index 0000000..2a9b90f --- /dev/null +++ b/AppleParty/AppleParty/RootView/Base.lproj/Main.storyboard @@ -0,0 +1,790 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/RootView/en.lproj/Main.strings b/AppleParty/AppleParty/RootView/en.lproj/Main.strings new file mode 100644 index 0000000..344c03a --- /dev/null +++ b/AppleParty/AppleParty/RootView/en.lproj/Main.strings @@ -0,0 +1,423 @@ + +/* Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; */ +"1UK-8n-QPP.title" = "Customize Toolbar…"; + +/* Class = "NSMenuItem"; title = "AppleParty"; ObjectID = "1Xt-HY-uBw"; */ +"1Xt-HY-uBw.title" = "AppleParty"; + +/* Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; */ +"1b7-l0-nxx.title" = "Find"; + +/* Class = "NSMenuItem"; title = "Lower"; ObjectID = "1tx-W0-xDw"; */ +"1tx-W0-xDw.title" = "Lower"; + +/* Class = "NSMenuItem"; title = "Raise"; ObjectID = "2h7-ER-AoG"; */ +"2h7-ER-AoG.title" = "Raise"; + +/* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ +"2oI-Rn-ZJC.title" = "Transformations"; + +/* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ +"3IN-sU-3Bg.title" = "Spelling"; + +/* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "3Om-Ey-2VK"; */ +"3Om-Ey-2VK.title" = "Use Default"; + +/* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ +"3rS-ZA-NoH.title" = "Speech"; + +/* Class = "NSMenuItem"; title = "Tighten"; ObjectID = "46P-cB-AYj"; */ +"46P-cB-AYj.title" = "Tighten"; + +/* Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; */ +"4EN-yA-p0u.title" = "Find"; + +/* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ +"4J7-dP-txa.title" = "Enter Full Screen"; + +/* Class = "NSMenuItem"; title = "Quit AppleParty"; ObjectID = "4sb-4s-VLi"; */ +"4sb-4s-VLi.title" = "Quit AppleParty"; + +/* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ +"5QF-Oa-p0T.title" = "Edit"; + +/* Class = "NSMenuItem"; title = "Copy Style"; ObjectID = "5Vv-lz-BsD"; */ +"5Vv-lz-BsD.title" = "Copy Style"; + +/* Class = "NSMenuItem"; title = "About AppleParty"; ObjectID = "5kV-Vb-QxS"; */ +"5kV-Vb-QxS.title" = "About AppleParty"; + +/* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ +"6dh-zS-Vam.title" = "Redo"; + +/* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ +"78Y-hA-62v.title" = "Correct Spelling Automatically"; + +/* Class = "NSMenu"; title = "Writing Direction"; ObjectID = "8mr-sm-Yjd"; */ +"8mr-sm-Yjd.title" = "Writing Direction"; + +/* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ +"9ic-FL-obx.title" = "Substitutions"; + +/* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ +"9yt-4B-nSM.title" = "Smart Copy/Paste"; + +/* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ +"AYu-sK-qS6.title" = "Main Menu"; + +/* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ +"BOF-NM-1cW.title" = "Preferences…"; + +/* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "BgM-ve-c93"; */ +"BgM-ve-c93.title" = "\tLeft to Right"; + +/* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "Bw7-FT-i3A"; */ +"Bw7-FT-i3A.title" = "Save As…"; + +/* Class = "NSMenuItem"; title = "Close"; ObjectID = "DVo-aG-piG"; */ +"DVo-aG-piG.title" = "Close"; + +/* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ +"Dv1-io-Yv7.title" = "Spelling and Grammar"; + +/* Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; */ +"F2S-fz-NVQ.title" = "Help"; + +/* Class = "NSMenuItem"; title = "AppleParty Help"; ObjectID = "FKE-Sm-Kum"; */ +"FKE-Sm-Kum.title" = "AppleParty Help"; + +/* Class = "NSMenuItem"; title = "Text"; ObjectID = "Fal-I4-PZk"; */ +"Fal-I4-PZk.title" = "Text"; + +/* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ +"FeM-D8-WVr.title" = "Substitutions"; + +/* Class = "NSMenuItem"; title = "Bold"; ObjectID = "GB9-OM-e27"; */ +"GB9-OM-e27.title" = "Bold"; + +/* Class = "NSMenu"; title = "Format"; ObjectID = "GEO-Iw-cKr"; */ +"GEO-Iw-cKr.title" = "Format"; + +/* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "GUa-eO-cwY"; */ +"GUa-eO-cwY.title" = "Use Default"; + +/* Class = "NSMenuItem"; title = "Font"; ObjectID = "Gi5-1S-RQB"; */ +"Gi5-1S-RQB.title" = "Font"; + +/* Class = "NSToolbarItem"; label = "Accounts"; ObjectID = "Gri-my-hqU"; */ +"Gri-my-hqU.label" = "Accounts"; + +/* Class = "NSToolbarItem"; paletteLabel = "Accounts"; ObjectID = "Gri-my-hqU"; */ +"Gri-my-hqU.paletteLabel" = "Accounts"; + +/* Class = "NSMenuItem"; title = "Writing Direction"; ObjectID = "H1b-Si-o9J"; */ +"H1b-Si-o9J.title" = "Writing Direction"; + +/* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ +"H8h-7b-M4v.title" = "View"; + +/* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ +"HFQ-gK-NFA.title" = "Text Replacement"; + +/* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ +"HFo-cy-zxI.title" = "Show Spelling and Grammar"; + +/* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ +"HyV-fh-RgO.title" = "View"; + +/* Class = "NSMenuItem"; title = "Subscript"; ObjectID = "I0S-gh-46l"; */ +"I0S-gh-46l.title" = "Subscript"; + +/* Class = "NSMenuItem"; title = "Open…"; ObjectID = "IAo-SY-fd9"; */ +"IAo-SY-fd9.title" = "Open…"; + +/* Class = "NSWindow"; title = "App Store Connect"; ObjectID = "IQv-IB-iLA"; */ +"IQv-IB-iLA.title" = "App Store Connect"; + +/* Class = "NSMenuItem"; title = "Justify"; ObjectID = "J5U-5w-g23"; */ +"J5U-5w-g23.title" = "Justify"; + +/* Class = "NSMenuItem"; title = "Use None"; ObjectID = "J7y-lM-qPV"; */ +"J7y-lM-qPV.title" = "Use None"; + +/* Class = "NSMenuItem"; title = "Revert to Saved"; ObjectID = "KaW-ft-85H"; */ +"KaW-ft-85H.title" = "Revert to Saved"; + +/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ +"Kd2-mp-pUS.title" = "Show All"; + +/* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ +"LE2-aR-0XJ.title" = "Bring All to Front"; + +/* Class = "NSMenuItem"; title = "Paste Ruler"; ObjectID = "LVM-kO-fVI"; */ +"LVM-kO-fVI.title" = "Paste Ruler"; + +/* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "Lbh-J2-qVU"; */ +"Lbh-J2-qVU.title" = "\tLeft to Right"; + +/* Class = "NSMenuItem"; title = "Copy Ruler"; ObjectID = "MkV-Pr-PK5"; */ +"MkV-Pr-PK5.title" = "Copy Ruler"; + +/* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ +"NMo-om-nkz.title" = "Services"; + +/* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "Nop-cj-93Q"; */ +"Nop-cj-93Q.title" = "\tDefault"; + +/* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ +"OY7-WF-poV.title" = "Minimize"; + +/* Class = "NSMenuItem"; title = "Baseline"; ObjectID = "OaQ-X3-Vso"; */ +"OaQ-X3-Vso.title" = "Baseline"; + +/* Class = "NSMenuItem"; title = "Hide AppleParty"; ObjectID = "Olw-nP-bQN"; */ +"Olw-nP-bQN.title" = "Hide AppleParty"; + +/* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; */ +"OwM-mh-QMV.title" = "Find Previous"; + +/* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ +"Oyz-dy-DGm.title" = "Stop Speaking"; + +/* Class = "NSMenuItem"; title = "Bigger"; ObjectID = "Ptp-SP-VEL"; */ +"Ptp-SP-VEL.title" = "Bigger"; + +/* Class = "NSMenuItem"; title = "Show Fonts"; ObjectID = "Q5e-8K-NDq"; */ +"Q5e-8K-NDq.title" = "Show Fonts"; + +/* Class = "NSToolbarItem"; label = "Settings"; ObjectID = "QN5-B3-zbW"; */ +"QN5-B3-zbW.label" = "Settings"; + +/* Class = "NSToolbarItem"; paletteLabel = "Settings"; ObjectID = "QN5-B3-zbW"; */ +"QN5-B3-zbW.paletteLabel" = "Settings"; + +/* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ +"R4o-n2-Eq4.title" = "Zoom"; + +/* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "RB4-Sm-HuC"; */ +"RB4-Sm-HuC.title" = "\tRight to Left"; + +/* Class = "NSMenuItem"; title = "Superscript"; ObjectID = "Rqc-34-cIF"; */ +"Rqc-34-cIF.title" = "Superscript"; + +/* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ +"Ruw-6m-B2m.title" = "Select All"; + +/* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; */ +"S0p-oC-mLd.title" = "Jump to Selection"; + +/* Class = "NSToolbarItem"; label = "GitHub"; ObjectID = "TCw-VP-4mw"; */ +"TCw-VP-4mw.label" = "GitHub"; + +/* Class = "NSToolbarItem"; paletteLabel = "GItHub"; ObjectID = "TCw-VP-4mw"; */ +"TCw-VP-4mw.paletteLabel" = "GItHub"; + +/* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ +"Td7-aD-5lo.title" = "Window"; + +/* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ +"UEZ-Bs-lqG.title" = "Capitalize"; + +/* Class = "NSMenuItem"; title = "Center"; ObjectID = "VIY-Ag-zcb"; */ +"VIY-Ag-zcb.title" = "Center"; + +/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ +"Vdr-fp-XzO.title" = "Hide Others"; + +/* Class = "NSMenuItem"; title = "Italic"; ObjectID = "Vjx-xi-njq"; */ +"Vjx-xi-njq.title" = "Italic"; + +/* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ +"W48-6f-4Dl.title" = "Edit"; + +/* Class = "NSMenuItem"; title = "Underline"; ObjectID = "WRG-CD-K1S"; */ +"WRG-CD-K1S.title" = "Underline"; + +/* Class = "NSMenuItem"; title = "New"; ObjectID = "Was-JA-tGl"; */ +"Was-JA-tGl.title" = "New"; + +/* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; */ +"WeT-3V-zwk.title" = "Paste and Match Style"; + +/* Class = "NSViewController"; title = "App Store Connect"; ObjectID = "XfG-lQ-9wD"; */ +"XfG-lQ-9wD.title" = "App Store Connect"; + +/* Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; */ +"Xz5-n4-O0W.title" = "Find…"; + +/* Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; */ +"YEy-JH-Tfz.title" = "Find and Replace…"; + +/* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "YGs-j5-SAR"; */ +"YGs-j5-SAR.title" = "\tDefault"; + +/* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ +"Ynk-f8-cLZ.title" = "Start Speaking"; + +/* Class = "NSMenuItem"; title = "Align Left"; ObjectID = "ZM1-6Q-yy1"; */ +"ZM1-6Q-yy1.title" = "Align Left"; + +/* Class = "NSMenuItem"; title = "Paragraph"; ObjectID = "ZvO-Gk-QUH"; */ +"ZvO-Gk-QUH.title" = "Paragraph"; + +/* Class = "NSMenuItem"; title = "Print…"; ObjectID = "aTl-1u-JFS"; */ +"aTl-1u-JFS.title" = "Print…"; + +/* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ +"aUF-d1-5bR.title" = "Window"; + +/* Class = "NSMenu"; title = "Font"; ObjectID = "aXa-aM-Jaq"; */ +"aXa-aM-Jaq.title" = "Font"; + +/* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "agt-UL-0e3"; */ +"agt-UL-0e3.title" = "Use Default"; + +/* Class = "NSMenuItem"; title = "Show Colors"; ObjectID = "bgn-CT-cEk"; */ +"bgn-CT-cEk.title" = "Show Colors"; + +/* Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu"; */ +"bib-Uj-vzu.title" = "File"; + +/* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; */ +"buJ-ug-pKt.title" = "Use Selection for Find"; + +/* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ +"c8a-y6-VQd.title" = "Transformations"; + +/* Class = "NSMenuItem"; title = "Use None"; ObjectID = "cDB-IK-hbR"; */ +"cDB-IK-hbR.title" = "Use None"; + +/* Class = "NSMenuItem"; title = "Selection"; ObjectID = "cqv-fj-IhA"; */ +"cqv-fj-IhA.title" = "Selection"; + +/* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ +"cwL-P1-jid.title" = "Smart Links"; + +/* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ +"d9M-CD-aMd.title" = "Make Lower Case"; + +/* Class = "NSMenu"; title = "Text"; ObjectID = "d9c-me-L2H"; */ +"d9c-me-L2H.title" = "Text"; + +/* Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ"; */ +"dMs-cI-mzQ.title" = "File"; + +/* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ +"dRJ-4n-Yzg.title" = "Undo"; + +/* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ +"gVA-U4-sdL.title" = "Paste"; + +/* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ +"hQb-2v-fYv.title" = "Smart Quotes"; + +/* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ +"hz2-CU-CR7.title" = "Check Document Now"; + +/* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ +"hz9-B4-Xy5.title" = "Services"; + +/* Class = "NSMenuItem"; title = "Smaller"; ObjectID = "i1d-Er-qST"; */ +"i1d-Er-qST.title" = "Smaller"; + +/* Class = "NSMenu"; title = "Baseline"; ObjectID = "ijk-EB-dga"; */ +"ijk-EB-dga.title" = "Baseline"; + +/* Class = "NSMenuItem"; title = "Kern"; ObjectID = "jBQ-r6-VK2"; */ +"jBQ-r6-VK2.title" = "Kern"; + +/* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "jFq-tB-4Kx"; */ +"jFq-tB-4Kx.title" = "\tRight to Left"; + +/* Class = "NSMenuItem"; title = "Format"; ObjectID = "jxT-CU-nIS"; */ +"jxT-CU-nIS.title" = "Format"; + +/* Class = "NSMenuItem"; title = "Show Sidebar"; ObjectID = "kIP-vf-haE"; */ +"kIP-vf-haE.title" = "Show Sidebar"; + +/* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ +"mK6-2p-4JG.title" = "Check Grammar With Spelling"; + +/* Class = "NSMenuItem"; title = "Ligatures"; ObjectID = "o6e-r0-MWq"; */ +"o6e-r0-MWq.title" = "Ligatures"; + +/* Class = "NSMenu"; title = "Open Recent"; ObjectID = "oas-Oc-fiZ"; */ +"oas-Oc-fiZ.title" = "Open Recent"; + +/* Class = "NSMenuItem"; title = "Loosen"; ObjectID = "ogc-rX-tC1"; */ +"ogc-rX-tC1.title" = "Loosen"; + +/* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ +"pa3-QI-u2k.title" = "Delete"; + +/* Class = "NSMenuItem"; title = "Save…"; ObjectID = "pxx-59-PXV"; */ +"pxx-59-PXV.title" = "Save…"; + +/* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; */ +"q09-fT-Sye.title" = "Find Next"; + +/* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "qIS-W8-SiK"; */ +"qIS-W8-SiK.title" = "Page Setup…"; + +/* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ +"rbD-Rh-wIN.title" = "Check Spelling While Typing"; + +/* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ +"rgM-f4-ycn.title" = "Smart Dashes"; + +/* Class = "NSMenuItem"; title = "Show Toolbar"; ObjectID = "snW-S8-Cw5"; */ +"snW-S8-Cw5.title" = "Show Toolbar"; + +/* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ +"tRr-pd-1PS.title" = "Data Detectors"; + +/* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "tXI-mr-wws"; */ +"tXI-mr-wws.title" = "Open Recent"; + +/* Class = "NSToolbarItem"; label = "Feedback"; ObjectID = "tg9-4q-8zI"; */ +"tg9-4q-8zI.label" = "Feedback"; + +/* Class = "NSToolbarItem"; paletteLabel = "Feedback"; ObjectID = "tg9-4q-8zI"; */ +"tg9-4q-8zI.paletteLabel" = "Feedback"; + +/* Class = "NSMenu"; title = "Kern"; ObjectID = "tlD-Oa-oAM"; */ +"tlD-Oa-oAM.title" = "Kern"; + +/* Class = "NSMenu"; title = "AppleParty"; ObjectID = "uQy-DD-JDr"; */ +"uQy-DD-JDr.title" = "AppleParty"; + +/* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ +"uRl-iY-unG.title" = "Cut"; + +/* Class = "NSMenuItem"; title = "Paste Style"; ObjectID = "vKC-jM-MkH"; */ +"vKC-jM-MkH.title" = "Paste Style"; + +/* Class = "NSMenuItem"; title = "Show Ruler"; ObjectID = "vLm-3I-IUL"; */ +"vLm-3I-IUL.title" = "Show Ruler"; + +/* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "vNY-rz-j42"; */ +"vNY-rz-j42.title" = "Clear Menu"; + +/* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ +"vmV-6d-7jI.title" = "Make Upper Case"; + +/* Class = "NSMenu"; title = "Ligatures"; ObjectID = "w0m-vy-SC9"; */ +"w0m-vy-SC9.title" = "Ligatures"; + +/* Class = "NSMenuItem"; title = "Align Right"; ObjectID = "wb2-vD-lq4"; */ +"wb2-vD-lq4.title" = "Align Right"; + +/* Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd"; */ +"wpr-3q-Mcd.title" = "Help"; + +/* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ +"x3v-GG-iWU.title" = "Copy"; + +/* Class = "NSMenuItem"; title = "Use All"; ObjectID = "xQD-1f-W4t"; */ +"xQD-1f-W4t.title" = "Use All"; + +/* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ +"xrE-MZ-jX0.title" = "Speech"; + +/* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ +"z6F-FW-3nz.title" = "Show Substitutions"; diff --git a/AppleParty/AppleParty/RootView/zh-Hans.lproj/Main.strings b/AppleParty/AppleParty/RootView/zh-Hans.lproj/Main.strings new file mode 100644 index 0000000..e63fb7d --- /dev/null +++ b/AppleParty/AppleParty/RootView/zh-Hans.lproj/Main.strings @@ -0,0 +1,435 @@ + +/* Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; */ +"1UK-8n-QPP.title" = "Customize Toolbar…"; + +/* Class = "NSMenuItem"; title = "AppleParty"; ObjectID = "1Xt-HY-uBw"; */ +"1Xt-HY-uBw.title" = "苹果派"; + +/* Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; */ +"1b7-l0-nxx.title" = "Find"; + +/* Class = "NSMenuItem"; title = "Lower"; ObjectID = "1tx-W0-xDw"; */ +"1tx-W0-xDw.title" = "Lower"; + +/* Class = "NSMenuItem"; title = "Raise"; ObjectID = "2h7-ER-AoG"; */ +"2h7-ER-AoG.title" = "Raise"; + +/* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ +"2oI-Rn-ZJC.title" = "Transformations"; + +/* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ +"3IN-sU-3Bg.title" = "Spelling"; + +/* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "3Om-Ey-2VK"; */ +"3Om-Ey-2VK.title" = "Use Default"; + +/* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ +"3rS-ZA-NoH.title" = "Speech"; + +/* Class = "NSMenuItem"; title = "Tighten"; ObjectID = "46P-cB-AYj"; */ +"46P-cB-AYj.title" = "Tighten"; + +/* Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; */ +"4EN-yA-p0u.title" = "Find"; + +/* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ +"4J7-dP-txa.title" = "Enter Full Screen"; + +/* Class = "NSMenuItem"; title = "Quit AppleParty"; ObjectID = "4sb-4s-VLi"; */ +"4sb-4s-VLi.title" = "退出苹果派"; + +/* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ +"5QF-Oa-p0T.title" = "Edit"; + +/* Class = "NSMenuItem"; title = "Copy Style"; ObjectID = "5Vv-lz-BsD"; */ +"5Vv-lz-BsD.title" = "Copy Style"; + +/* Class = "NSMenuItem"; title = "About AppleParty"; ObjectID = "5kV-Vb-QxS"; */ +"5kV-Vb-QxS.title" = "关于苹果派"; + +/* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ +"6dh-zS-Vam.title" = "Redo"; + +/* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ +"78Y-hA-62v.title" = "Correct Spelling Automatically"; + +/* Class = "NSMenu"; title = "Writing Direction"; ObjectID = "8mr-sm-Yjd"; */ +"8mr-sm-Yjd.title" = "Writing Direction"; + +/* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ +"9ic-FL-obx.title" = "Substitutions"; + +/* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ +"9yt-4B-nSM.title" = "Smart Copy/Paste"; + +/* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ +"AYu-sK-qS6.title" = "Main Menu"; + +/* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ +"BOF-NM-1cW.title" = "偏好设置"; + +/* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "BgM-ve-c93"; */ +"BgM-ve-c93.title" = "\tLeft to Right"; + +/* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "Bw7-FT-i3A"; */ +"Bw7-FT-i3A.title" = "Save As…"; + +/* Class = "NSMenuItem"; title = "Close"; ObjectID = "DVo-aG-piG"; */ +"DVo-aG-piG.title" = "Close"; + +/* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ +"Dv1-io-Yv7.title" = "Spelling and Grammar"; + +/* Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; */ +"F2S-fz-NVQ.title" = "Help"; + +/* Class = "NSMenuItem"; title = "AppleParty Help"; ObjectID = "FKE-Sm-Kum"; */ +"FKE-Sm-Kum.title" = "帮助文档"; + +/* Class = "NSMenuItem"; title = "Text"; ObjectID = "Fal-I4-PZk"; */ +"Fal-I4-PZk.title" = "Text"; + +/* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ +"FeM-D8-WVr.title" = "Substitutions"; + +/* Class = "NSMenuItem"; title = "Bold"; ObjectID = "GB9-OM-e27"; */ +"GB9-OM-e27.title" = "Bold"; + +/* Class = "NSMenu"; title = "Format"; ObjectID = "GEO-Iw-cKr"; */ +"GEO-Iw-cKr.title" = "Format"; + +/* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "GUa-eO-cwY"; */ +"GUa-eO-cwY.title" = "Use Default"; + +/* Class = "NSMenuItem"; title = "Font"; ObjectID = "Gi5-1S-RQB"; */ +"Gi5-1S-RQB.title" = "Font"; + +/* Class = "NSToolbarItem"; label = "Accounts"; ObjectID = "Gri-my-hqU"; */ +"Gri-my-hqU.label" = "账户"; + +/* Class = "NSToolbarItem"; paletteLabel = "Accounts"; ObjectID = "Gri-my-hqU"; */ +/* Class = "NSToolbarItem"; toolTip = "Log in or switch accounts"; ObjectID = "Gri-my-hqU"; */ +"Gri-my-hqU.toolTip" = "登陆或切换账号"; +"Gri-my-hqU.paletteLabel" = "账户"; + +/* Class = "NSMenuItem"; title = "Writing Direction"; ObjectID = "H1b-Si-o9J"; */ +"H1b-Si-o9J.title" = "Writing Direction"; + +/* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ +"H8h-7b-M4v.title" = "View"; + +/* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ +"HFQ-gK-NFA.title" = "Text Replacement"; + +/* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ +"HFo-cy-zxI.title" = "Show Spelling and Grammar"; + +/* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ +"HyV-fh-RgO.title" = "View"; + +/* Class = "NSMenuItem"; title = "Subscript"; ObjectID = "I0S-gh-46l"; */ +"I0S-gh-46l.title" = "Subscript"; + +/* Class = "NSMenuItem"; title = "Open…"; ObjectID = "IAo-SY-fd9"; */ +"IAo-SY-fd9.title" = "Open…"; + +/* Class = "NSWindow"; title = "AppleParty"; ObjectID = "IQv-IB-iLA"; */ +"IQv-IB-iLA.title" = "AppleParty"; + +/* Class = "NSMenuItem"; title = "Justify"; ObjectID = "J5U-5w-g23"; */ +"J5U-5w-g23.title" = "Justify"; + +/* Class = "NSMenuItem"; title = "Use None"; ObjectID = "J7y-lM-qPV"; */ +"J7y-lM-qPV.title" = "Use None"; + +/* Class = "NSMenuItem"; title = "Revert to Saved"; ObjectID = "KaW-ft-85H"; */ +"KaW-ft-85H.title" = "Revert to Saved"; + +/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ +"Kd2-mp-pUS.title" = "显示所有"; + +/* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ +"LE2-aR-0XJ.title" = "Bring All to Front"; + +/* Class = "NSMenuItem"; title = "Paste Ruler"; ObjectID = "LVM-kO-fVI"; */ +"LVM-kO-fVI.title" = "Paste Ruler"; + +/* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "Lbh-J2-qVU"; */ +"Lbh-J2-qVU.title" = "\tLeft to Right"; + +/* Class = "NSMenuItem"; title = "Copy Ruler"; ObjectID = "MkV-Pr-PK5"; */ +"MkV-Pr-PK5.title" = "Copy Ruler"; + +/* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ +"NMo-om-nkz.title" = "服务"; + +/* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "Nop-cj-93Q"; */ +"Nop-cj-93Q.title" = "\tDefault"; + +/* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ +"OY7-WF-poV.title" = "Minimize"; + +/* Class = "NSMenuItem"; title = "Baseline"; ObjectID = "OaQ-X3-Vso"; */ +"OaQ-X3-Vso.title" = "Baseline"; + +/* Class = "NSMenuItem"; title = "Hide AppleParty"; ObjectID = "Olw-nP-bQN"; */ +"Olw-nP-bQN.title" = "隐藏苹果派"; + +/* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; */ +"OwM-mh-QMV.title" = "Find Previous"; + +/* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ +"Oyz-dy-DGm.title" = "Stop Speaking"; + +/* Class = "NSMenuItem"; title = "Bigger"; ObjectID = "Ptp-SP-VEL"; */ +"Ptp-SP-VEL.title" = "Bigger"; + +/* Class = "NSMenuItem"; title = "Show Fonts"; ObjectID = "Q5e-8K-NDq"; */ +"Q5e-8K-NDq.title" = "Show Fonts"; + +/* Class = "NSToolbarItem"; label = "Settings"; ObjectID = "QN5-B3-zbW"; */ +"QN5-B3-zbW.label" = "设置"; + +/* Class = "NSToolbarItem"; paletteLabel = "Settings"; ObjectID = "QN5-B3-zbW"; */ +"QN5-B3-zbW.paletteLabel" = "设置"; + +/* Class = "NSToolbarItem"; toolTip = "App settings"; ObjectID = "QN5-B3-zbW"; */ +"QN5-B3-zbW.toolTip" = "App 设置"; + +/* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ +"R4o-n2-Eq4.title" = "Zoom"; + +/* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "RB4-Sm-HuC"; */ +"RB4-Sm-HuC.title" = "\tRight to Left"; + +/* Class = "NSMenuItem"; title = "Superscript"; ObjectID = "Rqc-34-cIF"; */ +"Rqc-34-cIF.title" = "Superscript"; + +/* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ +"Ruw-6m-B2m.title" = "Select All"; + +/* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; */ +"S0p-oC-mLd.title" = "Jump to Selection"; + +/* Class = "NSToolbarItem"; label = "GitHub"; ObjectID = "TCw-VP-4mw"; */ +"TCw-VP-4mw.label" = "开源地址"; + +/* Class = "NSToolbarItem"; paletteLabel = "GitHub"; ObjectID = "TCw-VP-4mw"; */ +"TCw-VP-4mw.paletteLabel" = "开源地址"; +"TCw-VP-4mw.label" = "开源地址"; + +/* Class = "NSToolbarItem"; toolTip = "Open app open source address"; ObjectID = "TCw-VP-4mw"; */ +"TCw-VP-4mw.toolTip" = "访问项目 GitHub 仓库"; + +/* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ +"Td7-aD-5lo.title" = "Window"; + +/* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ +"UEZ-Bs-lqG.title" = "Capitalize"; + +/* Class = "NSMenuItem"; title = "Center"; ObjectID = "VIY-Ag-zcb"; */ +"VIY-Ag-zcb.title" = "Center"; + +/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ +"Vdr-fp-XzO.title" = "隐藏其它"; + +/* Class = "NSMenuItem"; title = "Italic"; ObjectID = "Vjx-xi-njq"; */ +"Vjx-xi-njq.title" = "Italic"; + +/* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ +"W48-6f-4Dl.title" = "Edit"; + +/* Class = "NSMenuItem"; title = "Underline"; ObjectID = "WRG-CD-K1S"; */ +"WRG-CD-K1S.title" = "Underline"; + +/* Class = "NSMenuItem"; title = "New"; ObjectID = "Was-JA-tGl"; */ +"Was-JA-tGl.title" = "New"; + +/* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; */ +"WeT-3V-zwk.title" = "Paste and Match Style"; + +/* Class = "NSViewController"; title = "AppleParty"; ObjectID = "XfG-lQ-9wD"; */ +"XfG-lQ-9wD.title" = "AppleParty"; + +/* Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; */ +"Xz5-n4-O0W.title" = "Find…"; + +/* Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; */ +"YEy-JH-Tfz.title" = "Find and Replace…"; + +/* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "YGs-j5-SAR"; */ +"YGs-j5-SAR.title" = "\tDefault"; + +/* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ +"Ynk-f8-cLZ.title" = "Start Speaking"; + +/* Class = "NSMenuItem"; title = "Align Left"; ObjectID = "ZM1-6Q-yy1"; */ +"ZM1-6Q-yy1.title" = "Align Left"; + +/* Class = "NSMenuItem"; title = "Paragraph"; ObjectID = "ZvO-Gk-QUH"; */ +"ZvO-Gk-QUH.title" = "Paragraph"; + +/* Class = "NSMenuItem"; title = "Print…"; ObjectID = "aTl-1u-JFS"; */ +"aTl-1u-JFS.title" = "Print…"; + +/* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ +"aUF-d1-5bR.title" = "Window"; + +/* Class = "NSMenu"; title = "Font"; ObjectID = "aXa-aM-Jaq"; */ +"aXa-aM-Jaq.title" = "Font"; + +/* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "agt-UL-0e3"; */ +"agt-UL-0e3.title" = "Use Default"; + +/* Class = "NSMenuItem"; title = "Show Colors"; ObjectID = "bgn-CT-cEk"; */ +"bgn-CT-cEk.title" = "Show Colors"; + +/* Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu"; */ +"bib-Uj-vzu.title" = "File"; + +/* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; */ +"buJ-ug-pKt.title" = "Use Selection for Find"; + +/* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ +"c8a-y6-VQd.title" = "Transformations"; + +/* Class = "NSMenuItem"; title = "Use None"; ObjectID = "cDB-IK-hbR"; */ +"cDB-IK-hbR.title" = "Use None"; + +/* Class = "NSMenuItem"; title = "Selection"; ObjectID = "cqv-fj-IhA"; */ +"cqv-fj-IhA.title" = "Selection"; + +/* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ +"cwL-P1-jid.title" = "Smart Links"; + +/* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ +"d9M-CD-aMd.title" = "Make Lower Case"; + +/* Class = "NSMenu"; title = "Text"; ObjectID = "d9c-me-L2H"; */ +"d9c-me-L2H.title" = "Text"; + +/* Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ"; */ +"dMs-cI-mzQ.title" = "File"; + +/* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ +"dRJ-4n-Yzg.title" = "Undo"; + +/* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ +"gVA-U4-sdL.title" = "Paste"; + +/* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ +"hQb-2v-fYv.title" = "Smart Quotes"; + +/* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ +"hz2-CU-CR7.title" = "Check Document Now"; + +/* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ +"hz9-B4-Xy5.title" = "Services"; + +/* Class = "NSMenuItem"; title = "Smaller"; ObjectID = "i1d-Er-qST"; */ +"i1d-Er-qST.title" = "Smaller"; + +/* Class = "NSMenu"; title = "Baseline"; ObjectID = "ijk-EB-dga"; */ +"ijk-EB-dga.title" = "Baseline"; + +/* Class = "NSMenuItem"; title = "Kern"; ObjectID = "jBQ-r6-VK2"; */ +"jBQ-r6-VK2.title" = "Kern"; + +/* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "jFq-tB-4Kx"; */ +"jFq-tB-4Kx.title" = "\tRight to Left"; + +/* Class = "NSMenuItem"; title = "Format"; ObjectID = "jxT-CU-nIS"; */ +"jxT-CU-nIS.title" = "Format"; + +/* Class = "NSMenuItem"; title = "Show Sidebar"; ObjectID = "kIP-vf-haE"; */ +"kIP-vf-haE.title" = "Show Sidebar"; + +/* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ +"mK6-2p-4JG.title" = "Check Grammar With Spelling"; + +/* Class = "NSMenuItem"; title = "Ligatures"; ObjectID = "o6e-r0-MWq"; */ +"o6e-r0-MWq.title" = "Ligatures"; + +/* Class = "NSMenu"; title = "Open Recent"; ObjectID = "oas-Oc-fiZ"; */ +"oas-Oc-fiZ.title" = "Open Recent"; + +/* Class = "NSMenuItem"; title = "Loosen"; ObjectID = "ogc-rX-tC1"; */ +"ogc-rX-tC1.title" = "Loosen"; + +/* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ +"pa3-QI-u2k.title" = "Delete"; + +/* Class = "NSMenuItem"; title = "Save…"; ObjectID = "pxx-59-PXV"; */ +"pxx-59-PXV.title" = "Save…"; + +/* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; */ +"q09-fT-Sye.title" = "Find Next"; + +/* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "qIS-W8-SiK"; */ +"qIS-W8-SiK.title" = "Page Setup…"; + +/* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ +"rbD-Rh-wIN.title" = "Check Spelling While Typing"; + +/* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ +"rgM-f4-ycn.title" = "Smart Dashes"; + +/* Class = "NSMenuItem"; title = "Show Toolbar"; ObjectID = "snW-S8-Cw5"; */ +"snW-S8-Cw5.title" = "Show Toolbar"; + +/* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ +"tRr-pd-1PS.title" = "Data Detectors"; + +/* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "tXI-mr-wws"; */ +"tXI-mr-wws.title" = "Open Recent"; + +/* Class = "NSToolbarItem"; label = "Feedback"; ObjectID = "tg9-4q-8zI"; */ +"tg9-4q-8zI.label" = "反馈"; + +/* Class = "NSToolbarItem"; paletteLabel = "Feedback"; ObjectID = "tg9-4q-8zI"; */ +"tg9-4q-8zI.paletteLabel" = "反馈"; + +/* Class = "NSToolbarItem"; toolTip = "Question feedback or suggestion"; ObjectID = "tg9-4q-8zI"; */ +"tg9-4q-8zI.toolTip" = "问题反馈或建议"; + +/* Class = "NSMenu"; title = "Kern"; ObjectID = "tlD-Oa-oAM"; */ +"tlD-Oa-oAM.title" = "Kern"; + +/* Class = "NSMenu"; title = "AppleParty"; ObjectID = "uQy-DD-JDr"; */ +"uQy-DD-JDr.title" = "苹果派"; + +/* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ +"uRl-iY-unG.title" = "Cut"; + +/* Class = "NSMenuItem"; title = "Paste Style"; ObjectID = "vKC-jM-MkH"; */ +"vKC-jM-MkH.title" = "Paste Style"; + +/* Class = "NSMenuItem"; title = "Show Ruler"; ObjectID = "vLm-3I-IUL"; */ +"vLm-3I-IUL.title" = "Show Ruler"; + +/* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "vNY-rz-j42"; */ +"vNY-rz-j42.title" = "Clear Menu"; + +/* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ +"vmV-6d-7jI.title" = "Make Upper Case"; + +/* Class = "NSMenu"; title = "Ligatures"; ObjectID = "w0m-vy-SC9"; */ +"w0m-vy-SC9.title" = "Ligatures"; + +/* Class = "NSMenuItem"; title = "Align Right"; ObjectID = "wb2-vD-lq4"; */ +"wb2-vD-lq4.title" = "Align Right"; + +/* Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd"; */ +"wpr-3q-Mcd.title" = "Help"; + +/* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ +"x3v-GG-iWU.title" = "Copy"; + +/* Class = "NSMenuItem"; title = "Use All"; ObjectID = "xQD-1f-W4t"; */ +"xQD-1f-W4t.title" = "Use All"; + +/* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ +"xrE-MZ-jX0.title" = "Speech"; + +/* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ +"z6F-FW-3nz.title" = "Show Substitutions"; diff --git a/AppleParty/AppleParty/Shared/Info/APConstants.swift b/AppleParty/AppleParty/Shared/Info/APConstants.swift new file mode 100644 index 0000000..d62485c --- /dev/null +++ b/AppleParty/AppleParty/Shared/Info/APConstants.swift @@ -0,0 +1,15 @@ +// +// APConstants.swift +// AppleParty +// +// Created by HTC on 2022/4/7. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Foundation + +let kApplePartyGitHub = "https://github.com/37iOS/AppleParty" +let kApplePartyWiKi = "https://github.com/37iOS/AppleParty/wiki" +let kApplePartyNewIssues = "https://github.com/37iOS/AppleParty/issues/new/choose" +let k37MobileGamesSite = "https://www.37.com.cn" +let k37iOSTeamJueJinSite = "https://juejin.cn/user/1002387318511214" diff --git a/AppleParty/AppleParty/Shared/Info/InfoCenter.swift b/AppleParty/AppleParty/Shared/Info/InfoCenter.swift new file mode 100644 index 0000000..b0bbd52 --- /dev/null +++ b/AppleParty/AppleParty/Shared/Info/InfoCenter.swift @@ -0,0 +1,126 @@ +// +// InfoCenter.swift +// AppleParty +// +// Created by HTC on 2022/3/17. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Foundation + +let InfoCenterKey_Session_Key = "InfoCenterKey_Session_Key" +let InfoCenterKey_TrusDevice_Key = "InfoCenterKey_TrusDevice_Key" + +/// AppStoreConnect 密钥模型 +struct AppStoreConnectKey: Codable { + var aliasName: String + var issuerID: String + var privateKeyID: String + var privateKey: String + var isused: Bool + + mutating func model(_ asck: AppStoreConnectKey, _ isused: Bool) -> AppStoreConnectKey { + AppStoreConnectKey(aliasName: asck.aliasName, issuerID: asck.issuerID, privateKeyID: asck.privateKeyID, privateKey: asck.privateKey, isused: isused) + } +} + + +/// 信息模型 +struct AppleInfoSession: Codable { + var scnt: String + var sessionId: String + var cookies: Data + var ascKeys: [AppStoreConnectKey] +} + + +struct InfoCenter { + static var shared = InfoCenter() + + var session: AppleInfoSession { + set { + let encoder = JSONEncoder() + if let data = try? encoder.encode(newValue) { + #if DEBUG + UserDefaults.standard.set(data, forKey: InfoCenterKey_Session_Key) + #else + try? APUtil.keychain.set(data, key: InfoCenterKey_Session_Key) + #endif + } + } + get { + var data: Data? + #if DEBUG + data = UserDefaults.standard.data(forKey: InfoCenterKey_Session_Key) + #else + data = try? APUtil.keychain.getData(InfoCenterKey_Session_Key) + #endif + if let data = data, let model = try? JSONDecoder().decode(AppleInfoSession.self, from: data) { + return model + } else { + return AppleInfoSession(scnt: "", sessionId: "", cookies: Data(), ascKeys: []) + } + } + } + + var scnt: String { + get { + session.scnt + } + set { + session = AppleInfoSession(scnt: newValue, sessionId: session.sessionId, cookies: session.cookies, ascKeys: session.ascKeys) + } + } + + var sessionId: String { + get { + session.sessionId + } + set { + session = AppleInfoSession(scnt: session.scnt, sessionId: newValue, cookies: session.cookies, ascKeys: session.ascKeys) + } + } + + var cookies: [HTTPCookie] { + get { + let data = session.cookies + // NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, HTTPCookie.self], from: data) + // adopt NSSecureCoding. Class 'NSHTTPCookie' does not adopt it + let cookies = try? NSKeyedUnarchiver.unarchiveObject(with: data) + return cookies as? [HTTPCookie] ?? [] + } + set { + let cookieData = (try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: false)) ?? Data() + session = AppleInfoSession(scnt: session.scnt, sessionId: session.sessionId, cookies: cookieData, ascKeys: session.ascKeys) + } + } + + var ascKeys: [AppStoreConnectKey] { + get { + session.ascKeys + } + set { + session = AppleInfoSession(scnt: session.scnt, sessionId: session.sessionId, cookies: session.cookies, ascKeys: newValue) + } + } + + var currentASCKey: AppStoreConnectKey? { + get { + let accounts = self.ascKeys + let models = accounts.filter({ $0.isused == true }) + return models.first + } + } + + var trusDevice: Bool { + get { + if let trus = APUtil.defaults.bool(forKey: InfoCenterKey_TrusDevice_Key) { + return trus + } + return true //默认为信任设备 + } + set { + APUtil.defaults.set(newValue, forKey: InfoCenterKey_TrusDevice_Key) + } + } +} diff --git a/AppleParty/AppleParty/Shared/Info/UserCenter.swift b/AppleParty/AppleParty/Shared/Info/UserCenter.swift new file mode 100644 index 0000000..9d9906a --- /dev/null +++ b/AppleParty/AppleParty/Shared/Info/UserCenter.swift @@ -0,0 +1,129 @@ +// +// UserCenter.swift +// AppleParty +// +// Created by HTC on 2022/3/17. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Foundation + + +struct User { + var appleid: String + var password: String +} + +struct Provider { + var name: String + var providerId: String + var publicProviderId: String +} + +/// 专用密码模型 +struct SPassword: Codable { + var account: String + var password: String + var isused: Bool + + mutating func model(_ sp: SPassword, _ isused: Bool) -> SPassword { + SPassword(account: sp.account, password: sp.password, isused: isused) + } +} + +struct SPasswordModel: Codable { + /// 数据存储 + var list: [SPassword] +} + +private let UserCenterKey_HistoryUser_Key = "UserCenterKey_HistoryUser_Key" +private let UserCenterKey_Developer_Key = "UserCenterKey_Developer_Key" +private let UserCenterKey_AutoLogin_Key = "UserCenterKey_AutoLogin_Key" +private let UserCenterKey_AppStoreConnect_Key = "UserCenterKey_AppStoreConnect_Key" + +struct UserCenter { + static var shared = UserCenter() + /// 账号id + var accountPrsId = "" + /// 账号邮箱 + var accountEmail = "" + /// 账号所有子账号信息 + var accountProviders: [String: Any] = [:] + /// 账号登陆态是否有效 + var isAuthorized = false + + + // MARK: - 历史登录用户 + lazy var historyUser: [User] = { + if let value = try? APUtil.keychain.getString(UserCenterKey_HistoryUser_Key) { + let array = value.components(separatedBy: "|") + var result = [User]() + for temp in array { + result.append(User(appleid: temp.components(separatedBy: "_").first ?? "", password: temp.components(separatedBy: "_").last ?? "")) + } + return result + } + return [] + }() + + // MARK: - 登录用户 + var loginedUser: User { + mutating get { + return historyUser.first ?? User(appleid: "", password: "") + } + set { + historyUser = historyUser.filter{ $0.appleid != newValue.appleid } + historyUser.insert(newValue, at: 0) + let value = historyUser.map { $0.appleid + "_" + $0.password }.joined(separator: "|") + try? APUtil.keychain.set(value, key: UserCenterKey_HistoryUser_Key) + } + } + + + // MARK: - 开发者信息 + /// 开发者 id + var developerId = "" + /// 新的开发者id + var publicDeveloperId = "" + /// 开发者名字 + var developerName = "" + /// 开发者团队 id + var developerTeamId = "" + /// 苹果账号专用密码 + var currentSPassword: SPassword? { + get { + let accounts = self.secondaryPasswordList + let models = accounts.filter({ $0.isused == true }) + return models.first + } + } + // 所有的专用密码 + var secondaryPasswordList: [SPassword] { + set { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + if let data = try? encoder.encode(SPasswordModel(list: newValue)) { + try? APUtil.keychain.set(data, key: UserCenterKey_Developer_Key) + } + } + get { + if let listData = try? APUtil.keychain.getData(UserCenterKey_Developer_Key), + let model = try? JSONDecoder().decode(SPasswordModel.self, from: listData) { + return model.list + } else { + return [] + } + } + } + + // MARK: - 自动登录态 + var isFirstTime = true + var isAutoLogin: Bool { + get { + return APUtil.defaults.bool(forKey: UserCenterKey_AutoLogin_Key) ?? false + } + set { + APUtil.defaults.set(newValue, forKey: UserCenterKey_AutoLogin_Key) + } + } +} diff --git a/AppleParty/AppleParty/Shared/Network/APClient.swift b/AppleParty/AppleParty/Shared/Network/APClient.swift new file mode 100644 index 0000000..5f1b3bb --- /dev/null +++ b/AppleParty/AppleParty/Shared/Network/APClient.swift @@ -0,0 +1,536 @@ +// +// APClient.swift +// AppleParty +// +// Created by HTC on 2022/3/17. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Foundation +import Alamofire + + +let baseHeaders: HTTPHeaders = [ + "Content-Type": "application/json", + "X-Apple-Widget-Key": "e0b80c3bf78523bfe80974d320935bfa30add02e1bff88ec2166c6bd5a706c42", //目前固定值 + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Safari/605.1.15", + "Accept": "application/json, text/plain, */*;q=0.8", + "Accept-Language": "zh-CN,zh;q=0.9", + "Referer": "https://appstoreconnect.apple.com", + "X-Requested-By": "dev.apple.com", //分析数据下载必备 + "fetch-csrf-token": "1", // 销售量数据下载必备 + "uicomponentname": "measureDisplay" +] + + +struct APClientSession { + static var shared = APClientSession() + let session: Session + let config: URLSessionConfiguration + let policy = RetryPolicy() + let getEncoding = URLEncoding.httpBody + let postEncoding = JSONEncoding.default + + init() { + config = URLSessionConfiguration.af.default + var headers: HTTPHeaders = baseHeaders + if InfoCenter.shared.scnt.isNotEmpty, InfoCenter.shared.sessionId.isNotEmpty { +// headers.add(name: "scnt", value: InfoCenter.shared.scnt) +// headers.add(name: "X-Apple-ID-Session-Id", value: InfoCenter.shared.sessionId) + } + config.headers = headers + + for cookie in InfoCenter.shared.cookies { + config.httpCookieStorage?.setCookie(cookie) + } + + session = Session(configuration: config) + } +} + +enum AppListStatus { + case all + case available + case filter(_ query: String?) +} + +enum APClient { + // 初始化登录请求 + case signIn(account: String, password: String) + // 获取登录itCtx cookie + case signInSession + // 用于检查session是否过期 + case validateSession + // 查询/发送手机验证码 + case verifySecurityPhone(mode: String, phoneid: Int) + // 手机验证码验证 + case submitSecurityCode(code: SecurityCode) + // 信任登陆设备 + case trusDevice(isTrus: Bool) + // 开发者新闻 + case providerNews + // 账号合同消息 + case providerContractMessage + // 应用列表 + case appList(status: AppListStatus) + // 应用版本 + case appVersion(appid: String) + // 内购列表-新 + case inAppPurchase(appid: String, type: IAPSearchType) + // 内购详情-新 + case inAppPurchaseDetail(iapid: String) + // 内购商品的价格档位 + case inAppPurchasePrices(iapid: String) + // 内购列表-旧 + case iaps(appid: String) + // 开发者信息 + case ascProvider + case ascProviders + // 切换账号 + case switchProvider(publicProviderId: String) + // 游戏详细信息 + case appInfo(appid: String) + // app分析数据 + case appAnalytics(appid: String, measures: String, frequency: String, startTime: String, endTime: String, filters: [String:Any]? = nil, group: String? = nil, csv: Bool = true) + case initCSRF + // app销售趋势 + case appSalestrends(appid: String, measures: String, frequency: String, startTime: String, endTime: String, measuresKeys: [[String:Any]], groupKey: String? = nil, optionKeys: [[String:Any]]? = nil, vcubes: Int) + // 根据providerid查询供应商编号 + case sapVendorNumbers(providerId: String) + // 下载汇总财务报表 + case summaryFinancialReport(providerId: String, vendorNumber: String, year: String, month: String) + // 获取付款汇总信息 + case paymentConsolidation(providerId: String, vendorNumber: String, year: String, month: String) + // 生成财务详细报告 + case generateFinancialReport(providerId: String, vendorNumber: String, year: String, month: String, regionCurrencyIds: [String]) + // 查询财务报告生成状态 + case generateFinancialReportStatus(providerId: String, vendorNumber: String, uuid: String) + // 下载详细财务报告 + case detailFinancialReport(url: String) + // 用户详情 + case userDetail + // 银行列表 + case bankList(contentProviderPublicId: String) + // 银行账号 + case bankAccountNumber(contentProviderPublicId: String, bankAccountInfoId: String) + + + enum SecurityCode { + case device(code: String) + case sms(code: String, phoneNumberId: Int, mode: String) + + var urlPathComponent: String { + switch self { + case .device: return "trusteddevice" + case .sms: return "phone" + } + } + } + + enum IAPSearchType { + case all + case submit + } +} + +extension APClient { + func sessionMethod() -> HTTPMethod { + switch self { + case .signIn, .submitSecurityCode, .appAnalytics, .appSalestrends, .initCSRF, .switchProvider: + return .post + case .signInSession, .inAppPurchase, .iaps, .ascProvider, .ascProviders, .appInfo, .trusDevice, .providerNews, .providerContractMessage, .validateSession, .sapVendorNumbers, .summaryFinancialReport, .paymentConsolidation, .generateFinancialReport, .generateFinancialReportStatus, .detailFinancialReport, .appVersion, .bankList, .bankAccountNumber, .userDetail, .appList, .inAppPurchasePrices, .inAppPurchaseDetail: + return .get + case .verifySecurityPhone: + return .put + } + } + + func sessionEncode() -> ParameterEncoding { + if sessionMethod() == .get { + return APClientSession.shared.getEncoding + } else { + return APClientSession.shared.postEncoding + } + } + + func sessionHeaders(_ headers: HTTPHeaders) -> HTTPHeaders { + var newHeaders = headers + switch self { + case .initCSRF: + newHeaders.update(name: "fetch-csrf-token", value: "1") + case .switchProvider: + newHeaders.update(name: "X-Requested-With", value: "xsdr2$") + default: + break + } + return newHeaders + } + + func getUrl() -> String { + switch self { + case .signIn: + return "https://idmsa.apple.com/appleauth/auth/signin?isRememberMeEnabled=true" + case .signInSession: + return "https://appstoreconnect.apple.com/olympus/v1/session" + case .validateSession: + return "https://appstoreconnect.apple.com/olympus/v1/check" + case .verifySecurityPhone: + return "https://idmsa.apple.com/appleauth/auth/verify/phone" + case let .submitSecurityCode(code): + return "https://idmsa.apple.com/appleauth/auth/verify/\(code.urlPathComponent)/securitycode" + case let .appList(status): + switch status { + case .all: + return "https://appstoreconnect.apple.com/iris/v1/apps?limit=999" + case .available: + return "https://appstoreconnect.apple.com/iris/v1/apps?include=reviewSubmissions&limit=999&filter[removed]=false&filter[appStoreVersions.appStoreState]=READY_FOR_SALE" + case .filter(query: let query): + let filter = query ?? "limit=999&include=appStoreVersions&limit[appStoreVersions]=1" + return "https://appstoreconnect.apple.com/iris/v1/apps?\(filter)" + } + case let .appVersion(appid): + return "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/"+appid+"/overview" + case let .inAppPurchase(appid, type): + var filter = "" + if type == .all { + filter = "filter[canBeSubmitted]=true&limit=500&sort=-referenceName&include=inAppPurchaseReviewSubmission&" + } else if type == .submit { + filter = "fields[inAppPurchase]=referenceName,productId,inAppPurchaseType&filter[canBeSubmitted]=true&limit=500&sort=-referenceName&exists[inAppPurchaseReviewSubmission]=true&" + } + return "https://appstoreconnect.apple.com/iris/v1/apps/"+appid+"/inAppPurchase?"+filter + case let .iaps(appid): + return "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/"+appid+"/iaps" + case let .inAppPurchaseDetail(iapid): + return "https://appstoreconnect.apple.com/iris/v2/inAppPurchases/\(iapid)?include=inAppPurchaseLocalizations,content,promotedPurchase,appStoreReviewScreenshot,inAppPurchaseTaxCategoryInfo&limit[inAppPurchaseLocalizations]=200" + case let .inAppPurchasePrices(iapid): + return "https://appstoreconnect.apple.com/iris/v2/inAppPurchases/\(iapid)/prices?include=inAppPurchasePricePoint&filter[territory]=USA" + case .ascProvider: + return "https://appstoreconnect.apple.com/olympus/v1/actors?include=provider" + case .ascProviders: + return "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/users/itc" + case .switchProvider: + return "https://appstoreconnect.apple.com/olympus/v1/providerSwitchRequests" + case let .appInfo(appid): + return "https://appstoreconnect.apple.com/iris/v1/apps/"+appid+"?include=appStoreVersions&limit[appStoreVersions]=6" + case .appAnalytics(_, _, _, _, _, _, _, csv: let csv): + if csv { + return "https://appstoreconnect.apple.com/analytics/api/v1/data/time-series-csv" + } + return "https://appstoreconnect.apple.com/analytics/api/v1/data/time-series" + case .appSalestrends(param: let param): + return "https://appstoreconnect.apple.com/trends/gsf/salesTrendsApp/businessareas/InternetServices/subjectareas/iTunes/vcubes/\(param.vcubes)/timeseries" + case .initCSRF: + return "https://appstoreconnect.apple.com/trends/gsf/owasp/csrf-guard.js" + case let .trusDevice(isTrus): + guard isTrus else { + return "https://idmsa.apple.com/appleauth/auth/2sv/donttrust" + } + return "https://idmsa.apple.com/appleauth/auth/2sv/trust" + case .providerNews: + return "https://appstoreconnect.apple.com/olympus/v1/providerNews" + case .providerContractMessage: + return "https://appstoreconnect.apple.com/olympus/v1/contractMessages" + case .sapVendorNumbers(providerId: let providerId): + return "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/paymentConsolidation/providers/\(providerId)/sapVendorNumbers" + case .summaryFinancialReport(providerId: let providerId, vendorNumber: let vendorNumber, year: let year, month: let month): + return "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/wa/downloadFinancialReportPageCSV?contentProviderId=\(providerId)&sapVendorNumber=\(vendorNumber)&year=\(year)&month=\(month)" + case .paymentConsolidation(providerId: let providerId, vendorNumber: let vendorNumber, year: let year, month: let month): + return "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/paymentConsolidation/providers/\(providerId)/sapVendorNumbers/\(vendorNumber)?year=\(year)&month=\(month)" + case .generateFinancialReport(providerId: let providerId, vendorNumber: let vendorNumber, year: let year, month: let month, regionCurrencyIds: let regionCurrencyIds): + return "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/paymentConsolidation/providers/\(providerId)/sapVendorNumbers/\(vendorNumber)/reports?year=\(year)&month=\(month)®ionCurrencyIds=\(regionCurrencyIds.joined(separator: ","))&reportTypes=APP_STORE_REPORT&isDetailedConsolidatedReq=true" + case .generateFinancialReportStatus(providerId: let providerId, vendorNumber: let vendorNumber, uuid: let uuid): + return "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/paymentConsolidation/providers/\(providerId)/sapVendorNumbers/\(vendorNumber)/reports/\(uuid)/status" + case .detailFinancialReport(url: let url): + return "https://appstoreconnect.apple.com\(url)" + case .userDetail: + return "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/user/detail" + case .bankList(contentProviderPublicId: let id): + return "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/contentProviders/\(id)/bankAccounts?meta=constraints" + case .bankAccountNumber(contentProviderPublicId: let contentProviderPublicId, bankAccountInfoId: let bankAccountInfoId): + return "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/contentProviders/\(contentProviderPublicId)/decryptedBankAccounts/\(bankAccountInfoId)" + } + } + + func getParam() -> [String: Any] { + var payload: [String: Any] = [:] + switch self { + case let .signIn(account, password): + payload["accountName"] = account + payload["password"] = password + payload["rememberMe"] = true + case let .verifySecurityPhone(mode, phoneid): + payload["phoneNumber"] = ["id": phoneid] + payload["mode"] = mode //"sms" "voice" + case let .submitSecurityCode(code): + switch code { + case .device(let code): + payload["securityCode"] = ["code": code] + case .sms(let code, let phoneNumberId, let mode): + payload["securityCode"] = ["code": code] + payload["phoneNumber"] = ["id": phoneNumberId] + payload["mode"] = mode //"sms" "voice" + } + case let .switchProvider(publicProviderId): + payload["data"] = [ + "type": "providerSwitchRequests", + "relationships": [ + "provider": [ + "data": [ + "id": publicProviderId, + "type": "providers" + ] + ] + ] + ] + case let .appAnalytics(appid, measures, frequency, startTime, endTime, filters, group, _): + payload["adamId"] = [appid] + payload["measures"] = [measures] + payload["frequency"] = frequency + payload["startTime"] = startTime + payload["endTime"] = endTime + payload["group"] = group + if let filter = filters { + payload["dimensionFilters"] = [filter] + } + case let .appSalestrends(appid, measures, frequency, startTime, endTime, measuresKeys, groupKey, optionKeys, _): + payload["componentName"] = measures + payload["group"] = [] + if let group = groupKey { + payload["group"] = [ group ] + } + payload["measures"] = measuresKeys + payload["cubeName"] = "sales" + payload["cubeApiType"] = "TIMESERIES" + payload["interval"] = [ "key": frequency, "startDate": startTime, "endDate": endTime] + payload["filters"] = [ [ "dimensionKey": groupKey ?? "gross_adam_id_piano", "optionKeys": [ appid ] ] ] + if let options = optionKeys { + var filter = payload["filters"] as! Array<[String: Any]> + filter.append(contentsOf: options) + payload["filters"] = filter + } + default: + break + } + return payload + } +} + +extension APClient { + typealias CompletionHandler = (_ result: [String: Any], _ response: HTTPURLResponse?, _ error: NSError?) -> Void + + func request(showLoading: Bool = false, inView: NSView = currentView() , retry: Int = 3, completionHandler: CompletionHandler?) { + + if showLoading { + APHUD.showLoading(inView) + } + + APClientSession.shared.session.request(getUrl(), + method: sessionMethod(), + parameters: sessionMethod() == .get ? nil : getParam(), + encoding: sessionEncode(), + headers: sessionHeaders(APClientSession.shared.config.headers), + interceptor: APClientSession.shared.policy).response { (dataResponse) in + if showLoading { + APHUD.hideLoading() + } + + var json = [String: Any]() + if let jsonObject = try? JSONSerialization.jsonObject(with: dataResponse.data ?? Data()), let dict = jsonObject as? [String: Any] { + json = dict + } else { + if let jsonObject = try? JSONSerialization.jsonObject(with: dataResponse.data ?? Data()), let array = jsonObject as? [Any] { + json = ["data": array] + } else { + let dataStr = String(decoding: dataResponse.data ?? Data(), as: UTF8.self) + APLogs.shared.add("返回数据非json格式:\(dataStr)", printlog: true) + // 非 json 格式内容,但接口需要这个内容 + switch self { + case .initCSRF, .validateSession: + json = ["data": dataStr] + default: + break + } + } + } + + APLogs.shared.add("👉 [Response] \(string(from: dataResponse.response?.statusCode)) \(getUrl())", printlog: true) + let isRetry = networkOperation(json: json, response: dataResponse.response) + if isRetry && retry > 0 { + APLogs.shared.add("‼️ Retry request: \(getUrl())", printlog: true) + request(showLoading: showLoading, retry: retry - 1, completionHandler: completionHandler) + return + } + + // status code 统一处理 + if let code = dataResponse.response?.statusCode { + switch code { + case 200...299: + completionHandler?(json, dataResponse.response, nil) + case 401: + if case .switchProvider = self, retry > 0 { + APLogs.shared.add("‼️ Retry request: \(getUrl())", printlog: true) + request(showLoading: showLoading, retry: retry - 1, completionHandler: completionHandler) + return + } + + // 登录session已过期 + var errors = dictionaryArray(json["errors"]) + var msg = string(from: errors.first?["title"]) + if errors.count == 0 { + errors = dictionaryArray(json["serviceErrors"]) + msg = string(from: errors.first?["message"]) + } + completionHandler?(json, dataResponse.response, NSError.APClientError(.notAuthorized, msg)) + case 403: + completionHandler?(json, dataResponse.response, NSError.APClientError(.accountLocked)) + case 409: + completionHandler?(json, dataResponse.response, NSError.APClientError(.twoStepOrFactor)) + case 412: + completionHandler?(json, dataResponse.response, NSError.APClientError(.privacyAcknowledgementRequired)) + case 500...599: + if retry > 0 { + request(showLoading: showLoading, retry: retry - 1, completionHandler: completionHandler) + return + } + completionHandler?(json, dataResponse.response, NSError.APClientError(code == 503 ? .service503StatusCode : .serviceBadStatusCode, dataResponse.error?.localizedDescription ?? "")) + default: + completionHandler?(json, dataResponse.response, NSError.APClientError(.failure, dataResponse.error?.localizedDescription ?? "")) + } + } else { + completionHandler?(json, dataResponse.response, NSError.APClientError(.unknown, dataResponse.error?.localizedDescription ?? "")) + } + } + } + + func download(filePath: URL, retry: Int = 3, completionHandler: CompletionHandler?) { + + let destination: DownloadRequest.Destination = { _,_ in + return (filePath, [.createIntermediateDirectories, .removePreviousFile]) + } + + APClientSession.shared.session.download(getUrl(),headers: APClientSession.shared.config.headers, + interceptor: nil, requestModifier: nil, to: destination).response { dataResponse in + switch dataResponse.result { + case .success(_): + completionHandler?(["filePath":filePath.path], dataResponse.response, nil) + case let .failure(error): + completionHandler?([:], dataResponse.response, error as NSError) + } + } + } +} + + + +extension APClient { + func networkOperation(json: [String: Any], response: HTTPURLResponse?, config: URLSessionConfiguration = APClientSession.shared.config) -> Bool { + switch self { + case .signIn: + let status_code = int(from: response?.statusCode) + switch status_code { + case 200, 409, 503: + if let header = response?.headers { + APClientSession.shared.config.headers.update(name: "scnt", value: string(from: header["scnt"])) + APClientSession.shared.config.headers.update(name: "X-Apple-ID-Session-Id", value: string(from: header["X-Apple-ID-Session-Id"])) + InfoCenter.shared.scnt = string(from: header["scnt"]) + InfoCenter.shared.sessionId = string(from: header["X-Apple-ID-Session-Id"]) + } + default: break + } + case .submitSecurityCode, .switchProvider: + InfoCenter.shared.cookies = APClientSession.shared.config.httpCookieStorage?.cookies ?? [] + if case .switchProvider = self { + config.headers.update(name: "X-Requested-With", value: "xsdr2$") + } + case .signInSession: + if json.isNotEmpty { + InfoCenter.shared.cookies = APClientSession.shared.config.httpCookieStorage?.cookies ?? [] + UserCenter.shared.accountProviders = json + UserCenter.shared.accountPrsId = string(from: dictionary(json["user"])["prsId"]) + UserCenter.shared.accountEmail = string(from: dictionary(json["user"])["emailAddress"]) + UserCenter.shared.developerId = string(from: dictionary(json["provider"])["providerId"]) + UserCenter.shared.publicDeveloperId = string(from: dictionary(json["provider"])["publicProviderId"]) + UserCenter.shared.developerName = string(from: dictionary(json["provider"])["name"]) + } + case .appList: + if response?.statusCode != 200 { + return true + } + case .ascProvider: + let includeds = dictionaryArray(json["included"]) + for included in includeds { + let internalId = string(from: dictionary(included["attributes"])["internalId"]) + if internalId == UserCenter.shared.developerId { + UserCenter.shared.developerTeamId = string(from: dictionary(included["attributes"])["developerTeamId"]) + break + } + } + case .validateSession: + // 如果有 code ==200, 且 data: Unauthenticated 开头内容,则表示 sessio 过期,重试 + let status_code = int(from: response?.statusCode) + if status_code != 401 { + sleep(1) + return true + } + case .initCSRF: + if let data = json["data"] as? String, data.count > 0, data.hasPrefix("CSRF") { + let index = data.index(data.startIndex, offsetBy: 5) + let csrf = String(data.suffix(from: index)) + config.headers.update(name: "x-requested-with", value: "OWASP CSRFGuard Project") + config.headers.update(name: "csrf", value: csrf) + } + case .appSalestrends: + guard (json["result"] as? [[String: [Any]]]) != nil else { + return true + } + default: + break + } + + return false + } +} + + + +// MARK: - 请求错误码 +public enum APClientErrorCode: Int { + case failure = 400 + case notAuthorized = 401 + case accountLocked = 403 + case twoStepOrFactor = 409 + case privacyAcknowledgementRequired = 412 + case serviceBadStatusCode = 500 + case service503StatusCode = 503 + case notDeveloperAppleId = 9998 + case unknown = 9999 + + public var errorDescription: String { + switch self { + case .failure: + return "请求失败" + case .notAuthorized: + return "登陆过期,请重新登陆账号。" + case .accountLocked: + return "账号被禁用,详细请登陆 https://appstoreconnect.apple.com 了解更多。" + case .twoStepOrFactor: + return "需要进行双重认证。" + case .privacyAcknowledgementRequired: + return "账号需要同意新协议,详细请登陆 https://appstoreconnect.apple.com 了解更多。" + case .notDeveloperAppleId: + return "账号未注册 Apple 开发者!" + case .unknown: + return "未知错误" + case .serviceBadStatusCode: + return "服务端处理异常,请重试~" + case .service503StatusCode: + return "服务端503异常,请重试~" + } + } +} + +extension NSError { + static func APClientError(_ code: APClientErrorCode, _ message: String = "") -> NSError { + return NSError(domain: "com.AppleParty.error", code: code.rawValue, userInfo: [NSLocalizedDescriptionKey: message.count > 0 ? message : code.errorDescription]) + } +} diff --git a/AppleParty/AppleParty/Shared/Network/AppStoreConnectAPI.swift b/AppleParty/AppleParty/Shared/Network/AppStoreConnectAPI.swift new file mode 100644 index 0000000..f73e58d --- /dev/null +++ b/AppleParty/AppleParty/Shared/Network/AppStoreConnectAPI.swift @@ -0,0 +1,1334 @@ +// +// AppStoreConnectAPI.swift +// AppleParty +// +// Created by HTC on 2022/11/14. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// +// refer: https://github.com/AvdLee/appstoreconnect-swift-sdk + + +import Foundation +import Combine +import AppStoreConnect_Swift_SDK + + +typealias ASCApp = AppStoreConnect_Swift_SDK.App +typealias ASCTerritory = AppStoreConnect_Swift_SDK.Territory + +typealias ASCInAppPurchaseV2 = AppStoreConnect_Swift_SDK.InAppPurchaseV2 +typealias ASCIAPPricePoint = AppStoreConnect_Swift_SDK.InAppPurchasePricePoint +typealias ASCIAPPriceSchedule = AppStoreConnect_Swift_SDK.InAppPurchasePriceSchedule +typealias ASCIAPLocalization = AppStoreConnect_Swift_SDK.InAppPurchaseLocalization +typealias ASCIAPScreenshot = AppStoreConnect_Swift_SDK.InAppPurchaseAppStoreReviewScreenshot +typealias ASCIAPAvailability = AppStoreConnect_Swift_SDK.InAppPurchaseAvailability + +typealias ASCSubscription = AppStoreConnect_Swift_SDK.Subscription +typealias ASCSubscriptionPrice = AppStoreConnect_Swift_SDK.SubscriptionPrice +typealias ASCSubscriptionPricePoint = AppStoreConnect_Swift_SDK.SubscriptionPricePoint +typealias ASCSubscriptionLocalization = AppStoreConnect_Swift_SDK.SubscriptionLocalization +typealias ASCSubscriptionScreenshot = AppStoreConnect_Swift_SDK.SubscriptionAppStoreReviewScreenshot +typealias ASCSubscriptionGroup = AppStoreConnect_Swift_SDK.SubscriptionGroup +typealias ASCSubscriptionGroupLocale = AppStoreConnect_Swift_SDK.SubscriptionGroupLocalization +typealias ASCSubscriptionAvailability = AppStoreConnect_Swift_SDK.SubscriptionAvailability + + +class APASCAPI { + + public var updateMsg: (([String])->Void)? + private var message = [String]() { + didSet { + updateMsg?(message) + } + } + + // 日志文件保存路径 + private let date = Date() + var logPath: String { + get { + let dateFormatter : DateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMdd_HHmmss" + let currentDate = dateFormatter.string(from: date) + return "/AppleParty/ASCAPI/log_\(currentDate).txt" + } + } + + /// Go to https://appstoreconnect.apple.com/access/api and create your own key. This is also the page to find the private key ID and the issuer ID. + /// Download the private key and open it in a text editor. Remove the enters and copy the contents over to the private key parameter. + //private let configuration = APIConfiguration(issuerID: "", privateKeyID: "", privateKey: "") + //private lazy var provider: APIProvider = APIProvider(configuration: configuration) + private var provider: APIProvider? + private var rateLimitPublisher: AnyCancellable? + + init(issuerID: String, privateKeyID: String, privateKey: String, showApiRateLimit: Bool) { + do { + let configuration = try APIConfiguration(issuerID: issuerID, privateKeyID: privateKeyID, privateKey: privateKey) + self.provider = APIProvider(configuration: configuration) + if showApiRateLimit { + self.rateLimitPublisher = self.provider?.rateLimitPublisher.sink(receiveValue: { [weak self] rateLimit in + self?.handleError("接口速率限制:\(rateLimit.requestURL?.relativePath ?? ""), \(rateLimit.entries)") + }) + } + } catch { + handleError("初始化失败: \(error.localizedDescription)") + } + } + + /// 获取所有 app + /// - Returns: apps + func apps() async -> [ASCApp]? { + let request = APIEndpoint.v1.apps + .get(parameters: .init( + sort: [.bundleID], + fieldsApps: [.appInfos, .name, .bundleID], + limit: 200 + )) + do { + guard let provider = provider else { + return nil + } + var allApps: [ASCApp] = [] + for try await pagedResult in provider.paged(request) { + allApps.append(contentsOf: pagedResult.data) + } + return allApps + } catch { + handleError("获取app失败: \(error.localizedDescription)") + return nil + } + } + + /// 获取所有 App Store 支持的国家或地区 + /// - Returns: apps + func territories() async -> [ASCTerritory]? { + let request = APIEndpoint.v1.territories + .get(limit: 200) + do { + guard let provider = provider else { + return nil + } + var allTerritorys: [ASCTerritory] = [] + for try await pagedResult in provider.paged(request) { + allTerritorys.append(contentsOf: pagedResult.data) + } + return allTerritorys + } catch { + handleError("获取 App Store 支持的国家或地区失败: \(error.localizedDescription)") + return nil + } + } + + // MARK: - 非续期订阅类型内购 API + + /// 获取 app 所有的商品信息 + /// - Parameter appId: apple id + /// - Returns: 商品列表 + func fetchInAppPurchasesList(appId: String) async -> [ASCInAppPurchaseV2] { + let request = APIEndpoint.v1.apps.id(appId).inAppPurchasesV2 + .get(parameters: .init( + limit: 200 + )) + do { + guard let provider = provider else { + return [] + } + var allIAPs: [ASCInAppPurchaseV2] = [] + for try await pagedResult in provider.paged(request) { + allIAPs.append(contentsOf: pagedResult.data) + } + return allIAPs + } catch { + handleError("获取内购列表失败: \(error.localizedDescription)") + return [] + } + + } + + /// 创建内购商品 + /// - Parameters: + /// - appId: apple id + /// - product: 内购商品信息 + /// - Returns: 成功时返回对应的商品信息 + func createInAppPurchases(appId: String, product: IAPProduct) async -> ASCInAppPurchaseV2? { + let body = [ + "data": [ + "attributes": [ + //"availableInAllTerritories": product.territories.availableInAllTerritories, + "familySharable": product.familySharable, + // CONSUMABLE、NON_CONSUMABLE、NON_RENEWING_SUBSCRIPTION + "inAppPurchaseType": product.inAppPurchaseType.rawValue, + "name": product.name, + "productId": product.productId, + "reviewNote": product.reviewNote, + ], + "relationships": [ + "app": [ + "data": [ + "id": appId, + "type": "apps" + ] + ] + ], + "type": "inAppPurchases" + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(InAppPurchaseV2CreateRequest.self, from: json) + let request = APIEndpoint.v2.inAppPurchases.post(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("创建内购商品失败: \(error.localizedDescription)") + } + return nil + } + + + /// 修改内购商品 + /// - Parameters: + /// - iapId: 内购商品 id + /// - product: 内购商品信息 + /// - Returns: 成功时返回对应的商品信息 + func updateInAppPurchases(iapId: String, product: IAPProduct) async -> ASCInAppPurchaseV2? { + let body = [ + "data": [ + "attributes": [ + //"availableInAllTerritories": product.territories.availableInAllTerritories, + "familySharable": product.familySharable, + "name": product.name, + "reviewNote": product.reviewNote, + ], + "id": iapId, + "type": "inAppPurchases" + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(InAppPurchaseV2UpdateRequest.self, from: json) + let request = APIEndpoint.v2.inAppPurchases.id(iapId).patch(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("修改内购商品信息失败: \(error.localizedDescription)") + } + return nil + } + + + /// 删除内购商品 + /// - Parameters: + /// - iapId: 内购商品 id + /// - Returns: 返回对应的状态码,成功时返回 204 + func deleteInAppPurchases(iapId: String) async -> Int { + do { + guard let provider = provider else { + return 400 + } + let request = APIEndpoint.v2.inAppPurchases.id(iapId).delete + try await provider.request(request) + return 204 + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + return statusCode + } catch { + handleError("删除内购商品失败: \(error.localizedDescription)") + } + return 400 + } + + + /// 获取内购商品价格档位 + /// - Parameters: + /// - iapId: 内购商品 id + /// - territory: 国家或地区 + /// - Returns: 成功时返回对应的档位信息 + func fetchPricePoints(iapId: String, territory: [String]?) async -> [ASCIAPPricePoint] { + let request = APIEndpoint.v2.inAppPurchases.id(iapId).pricePoints + .get(parameters: .init( + filterTerritory: territory, + limit: 200, + include: [.territory] + )) + do { + guard let provider = provider else { + return [] + } + var allApps: [ASCIAPPricePoint] = [] + for try await pagedResult in provider.paged(request) { + allApps.append(contentsOf: pagedResult.data) + } + return allApps + } catch { + handleError("获取内购商品价格档位失败: \(error.localizedDescription)") + return [] + } + } + + + /// 构建一个价格计划表 + /// - Parameters: + /// - scheduleId: 价格计划表 id + /// - pricePointId: 价格点 id + /// - iapId: 内购商品 id + /// - Returns: 价格计划表字典 + func fetchInAppPurchasePriceSchedule(scheduleId: String, pricePointId: String, iapId: String, index: Int) -> [String: Any] { + [ + "id": "${\(scheduleId)-\(index)}", + "type": "inAppPurchasePrices", + "attributes": [ + "startDate": nil, + "endDate": nil + ], + "relationships": [ + "inAppPurchasePricePoint": [ + "data": [ + "id": pricePointId, + "type": "inAppPurchasePricePoints" + ] + ], + "inAppPurchaseV2": [ + "data": [ + "id": iapId, + "type": "inAppPurchases" + ] + ] + ] + ] + } + + + /// 修改内购商品价格档位 + /// - Parameters: + /// - iapId: 内购商品 id + /// - baseTerritoryId: 价格档位标识(自定义名字) + /// - manualPrices: 价格档位 id + /// - included: 内购商品信息 + /// - Returns: 成功时返回对应的档位信息 + func updateInAppPurchasePricePoint(iapId: String, baseTerritoryId: String, manualPrices: [Any], included: [Any]) async -> ASCIAPPriceSchedule? { + let body: [String : Any] = [ + "data": [ + "type": "inAppPurchasePriceSchedules", + "relationships": [ + "inAppPurchase": [ + "data": [ + "id": iapId, + "type": "inAppPurchases" + ] + ], + "baseTerritory": [ + "data": [ + "id": baseTerritoryId, + "type": "territories" + ] + ], + "manualPrices": [ + "data": manualPrices +// [ +// [ +// "id": priceTierId, +// "type": "inAppPurchasePrices" +// ] +// ] + ] + ] + ], + "included": included +// [ +// [ +// "id": priceTierId, +// "type": "inAppPurchasePrices", +// "attributes": [ +// "startDate": nil, +// "endDate": nil +// ], +// "relationships": [ +// "inAppPurchasePricePoint": [ +// "data": [ +// "id": pricePointId, +// "type": "inAppPurchasePricePoints" +// ] +// ], +// "inAppPurchaseV2": [ +// "data": [ +// "id": iapId, +// "type": "inAppPurchases" +// ] +// ] +// ] +// ] +// ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(InAppPurchasePriceScheduleCreateRequest.self, from: json) + let request = APIEndpoint.v1.inAppPurchasePriceSchedules.post(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("修改内购商品价格档位失败: \(error.localizedDescription)") + } + return nil + } + + + /// 获取内购商品本地化信息 + /// - Parameters: + /// - iapId: 内购商品 id + /// - Returns: 成功时返回对应的所有商品本地化信息 + func fetchInAppPurchasesLocalizations(iapId: String) async -> [ASCIAPLocalization] { + let request = APIEndpoint.v2.inAppPurchases.id(iapId).inAppPurchaseLocalizations + .get(parameters: .init( + limit: 200 + )) + do { + guard let provider = provider else { + return [] + } + var allApps: [ASCIAPLocalization] = [] + for try await pagedResult in provider.paged(request) { + allApps.append(contentsOf: pagedResult.data) + } + return allApps + } catch { + handleError("获取内购商品本地化信息失败: \(error.localizedDescription)") + return [] + } + } + + + /// 创建内购商品本地化 + /// - Parameters: + /// - iapId: 内购商品 id + /// - localization: 本地化信息模型 + /// - Returns: 成功时返回对应的本地化信息 + func createInAppPurchasesLocalization(iapId: String, localization: IAPLocalization) async -> ASCIAPLocalization? { + let body = [ + "data": [ + "attributes": [ + "name": localization.name, + "description": localization.description, + "locale": localization.locale + ], + "relationships": [ + "inAppPurchaseV2": [ + "data": [ + "id": iapId, + "type": "inAppPurchases" + ] + ] + ], + "type": "inAppPurchaseLocalizations" + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(InAppPurchaseLocalizationCreateRequest.self, from: json) + let request = APIEndpoint.v1.inAppPurchaseLocalizations.post(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("创建内购商品本地化信息失败:\(error.localizedDescription)") + } + return nil + } + + + /// 修改内购商品本地化信息 + /// - Parameters: + /// - iapLocaleId: 内购商品本地化语言 id + /// - localization: 内购商品本地化模型 + /// - Returns: 成功时返回对应的本地化信息 + func updateInAppPurchasesLocalization(iapLocaleId: String, localization: IAPLocalization) async -> ASCIAPLocalization? { + let body = [ + "data": [ + "attributes": [ + "name": localization.name, + "description": localization.description, + ], + "id": iapLocaleId, + "type": "inAppPurchaseLocalizations" + ] + ] + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(InAppPurchaseLocalizationUpdateRequest.self, from: json) + let request = APIEndpoint.v1.inAppPurchaseLocalizations.id(iapLocaleId).patch(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("修改内购商品本地化信息失败: \(error.localizedDescription)") + } + return nil + } + + + /// 删除内购商品本地化 + /// - Parameters: + /// - iapLocaleId: 内购商品本地化语言 id + /// - Returns: 返回对应的状态码,成功时返回 204 + func deleteInAppPurchasesLocalization(iapLocaleId: String) async -> Int { + do { + guard let provider = provider else { + return 400 + } + let request = APIEndpoint.v1.inAppPurchaseLocalizations.id(iapLocaleId).delete + try await provider.request(request) + return 204 + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + return statusCode + } catch { + handleError("删除内购商品本地化信息失败:\(error.localizedDescription)") + } + return 400 + } + + + /// 获取内购商品的送审截屏 + /// - Parameter iapId: 内购商品 id + /// - Returns: 返回内购商品的截图信息 + func fetchInAppPurchasesScreenshot(iapId: String) async -> ASCIAPScreenshot? { + let request = APIEndpoint.v2.inAppPurchases.id(iapId).appStoreReviewScreenshot.get() + do { + guard let provider = provider else { + return nil + } + let shot = try await provider.request(request).data + return shot + } catch { + handleError("获取内购商品的送审截图异常: \(error.localizedDescription)") + return nil + } + } + + /// 创建内购商品的送审截屏 + /// - Parameters: + /// - iapId: 内购商品 id + /// - fileName: 截图名称 + /// - fileSize: 截图大小 + /// - Returns: 返回预创建的内购商品的截图信息 + func createInAppPurchasesScreenshot(iapId: String, fileName: String, fileSize: Int) async -> ASCIAPScreenshot? { + let body = [ + "data": [ + "attributes": [ + "fileName": fileName, + "fileSize": fileSize, + ], + "relationships": [ + "inAppPurchaseV2": [ + "data": [ + "id": iapId, + "type": "inAppPurchases" + ] + ] + ], + "type": "inAppPurchaseAppStoreReviewScreenshots" + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(InAppPurchaseAppStoreReviewScreenshotCreateRequest.self, from: json) + let request = APIEndpoint.v1.inAppPurchaseAppStoreReviewScreenshots.post(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("创建内购商品的送审截图失败:\(error.localizedDescription)") + } + return nil + } + + + /// 确认内购商品的送审截屏 + /// - Parameters: + /// - iapShotId: 内购商品截屏 id + /// - fileMD5: 内购截屏文件 md5 值 + /// - Returns: 成功时返回对应的截屏模型信息 + func updateInAppPurchasesScreenshot(iapShotId: String, fileMD5: String) async -> ASCIAPScreenshot? { + let body = [ + "data": [ + "attributes": [ + "uploaded": true, + "sourceFileChecksum": fileMD5, + ], + "id": iapShotId, + "type": "inAppPurchaseAppStoreReviewScreenshots" + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(InAppPurchaseAppStoreReviewScreenshotUpdateRequest.self, from: json) + let request = APIEndpoint.v1.inAppPurchaseAppStoreReviewScreenshots.id(iapShotId).patch(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("修改内购商品的送审截图失败: \(error.localizedDescription)") + } + return nil + } + + /// 删除内购商品的送审截屏 + /// - Parameters: + /// - iapShotId: 内购商品截屏 id + /// - Returns: 成功时返回对应的截屏模型信息 + func deleteInAppPurchasesScreenshot(iapShotId: String) async -> Int { + do { + guard let provider = provider else { + return 400 + } + let request = APIEndpoint.v1.inAppPurchaseAppStoreReviewScreenshots.id(iapShotId).delete + try await provider.request(request) + return 204 + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + return statusCode + } catch { + handleError("删除内购商品的送审截图失败:\(error.localizedDescription)") + } + return 400 + } + + + /// 修改内购商品的销售范围 + /// - Parameters: + /// - iapId: 内购商品 id + /// - availableTerritories: 可供销售的国家和地区列表 + /// - availableInNewTerritories: 是否在将来的所有 App Store 国家或地区中自动提供 App 内购买 + /// - Returns: 成功时返回对应的模型 + func updateInAppPurchasesAvailabilityTerritories(iapId: String, availableTerritories: [Any], availableInNewTerritories: Bool) async -> ASCIAPAvailability? { + let body = [ + "data": [ + "type": "inAppPurchaseAvailabilities", + "attributes": [ + "availableInNewTerritories": availableInNewTerritories + ], + "relationships": [ + "availableTerritories": [ + "data": availableTerritories +// [ +// [ +// "type": "territories", +// "id": "CHN" +// ] +// ] + ], + "inAppPurchase": [ + "data": [ + "id": iapId, + "type": "inAppPurchases" + ] + ] + ] + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(InAppPurchaseAvailabilityCreateRequest.self, from: json) + let request = APIEndpoint.v1.inAppPurchaseAvailabilities.post(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("修改内购商品的销售范围失败: \(error.localizedDescription)") + } + return nil + } + + + // MARK: - 续期订阅类商品 API + + + /// 获取所有订阅组 + /// - Parameters: + /// - appId: apple id + /// - Returns: 获取所有订阅组 + func fetchSubscriptionGroups(appId: String) async -> [ASCSubscriptionGroup] { + let request = APIEndpoint.v1.apps.id(appId).subscriptionGroups + .get(parameters: .init( + limit: 200 + )) + do { + guard let provider = provider else { + return [] + } + var allApps: [ASCSubscriptionGroup] = [] + for try await pagedResult in provider.paged(request) { + allApps.append(contentsOf: pagedResult.data) + } + return allApps + } catch { + handleError("获取所有订阅组失败: \(error.localizedDescription)") + return [] + } + } + + + /// 创建订阅组 + /// - Parameters: + /// - appId: apple id + /// - groupName: 订阅组名字 + /// - Returns: 成功时返回订阅组 + func createSubscriptionGroups(appId: String, groupName: String = "VIP") async -> ASCSubscriptionGroup? { + let body = [ + "data": [ + "attributes": [ + "referenceName": groupName + ], + "relationships": [ + "app": [ + "data": [ + "id": appId, + "type": "apps" + ] + ] + ], + "type": "subscriptionGroups" + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(SubscriptionGroupCreateRequest.self, from: json) + let request = APIEndpoint.v1.subscriptionGroups.post(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("创建订阅组失败:\(error.localizedDescription)") + } + return nil + } + + + /// 获取所有订阅组的本地化信息 + /// - Parameters: + /// - iapGroupId: 订阅组 id + /// - Returns: 获取所有订阅组 + func fetchSubscriptionGroupLocalizations(iapGroupId: String) async -> [ASCSubscriptionGroupLocale] { + let request = APIEndpoint.v1.subscriptionGroups.id(iapGroupId).subscriptionGroupLocalizations + .get(parameters: .init( + limit: 200 + )) + do { + guard let provider = provider else { + return [] + } + var allApps: [ASCSubscriptionGroupLocale] = [] + for try await pagedResult in provider.paged(request) { + allApps.append(contentsOf: pagedResult.data) + } + return allApps + } catch { + handleError("获取所有订阅组的本地化信息失败: \(error.localizedDescription)") + return [] + } + } + + + /// 创建订阅组本地化信息 + /// - Parameters: + /// - iapGroupId: 订阅组 id + /// - name: 订阅组本地化 id + /// - locale: 本地化标识 + /// - customAppName: 自定义 app 名字 + /// - Returns: 成功时返回订阅组本地化 + func createSubscriptionGroupLocalizations(iapGroupId: String, name: String = "VIP", locale: String = "zh-Hans", customAppName: String?) async -> ASCSubscriptionGroupLocale? { + let body = [ + "data": [ + "attributes": [ + "customAppName": customAppName, + "name": name, + "locale": locale + ], + "relationships": [ + "subscriptionGroup": [ + "data": [ + "id": iapGroupId, + "type": "subscriptionGroups" + ] + ] + ], + "type": "subscriptionGroupLocalizations" + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(SubscriptionGroupLocalizationCreateRequest.self, from: json) + let request = APIEndpoint.v1.subscriptionGroupLocalizations.post(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("创建订阅组本地化信息失败:\(error.localizedDescription)") + } + return nil + } + + + /// 获取订阅组下所有内购商品 + /// - Parameters: + /// - appId: apple id + /// - Returns: 获取所有订阅 + func fetchSubscriptionGroupSubscriptions(iapGroupId: String) async -> [ASCSubscription] { + let request = APIEndpoint.v1.subscriptionGroups.id(iapGroupId).subscriptions + .get(parameters: .init( + limit: 200 + )) + do { + guard let provider = provider else { + return [] + } + var allApps: [ASCSubscription] = [] + for try await pagedResult in provider.paged(request) { + allApps.append(contentsOf: pagedResult.data) + } + return allApps + } catch { + handleError("获取订阅组下所有内购商品失败: \(error.localizedDescription)") + return [] + } + } + + + /// 创建订阅商品 + /// - Parameters: + /// - iapGroupId: apple id + /// - product: 内购商品信息 + /// - Returns: 获取订阅 + func createSubscription(iapGroupId: String, product: IAPProduct) async -> ASCSubscription? { + let body = [ + "data": [ + "attributes": [ + "name": product.name, + "productId": product.productId, + "subscriptionPeriod": product.subscriptions?.subscriptionPeriod ?? "ONE_MONTH", + "familySharable": product.familySharable, + "reviewNote": product.reviewNote, + "groupLevel": product.subscriptions?.groupLevel ?? 1, + //"availableInAllTerritories": product.territories.availableInAllTerritories + ], + "relationships": [ + "group": [ + "data": [ + "id": iapGroupId, + "type": "subscriptionGroups" + ] + ] + ], + "type": "subscriptions" + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(SubscriptionCreateRequest.self, from: json) + let request = APIEndpoint.v1.subscriptions.post(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("创建订阅商品失败:\(error.localizedDescription)") + } + return nil + } + + + func updateSubscription(iapId: String, product: IAPProduct) async -> ASCSubscription? { + let body = [ + "data": [ + "attributes": [ + "name": product.name, + "subscriptionPeriod": product.subscriptions?.subscriptionPeriod ?? "ONE_MONTH", + "familySharable": product.familySharable, + "reviewNote": product.reviewNote, + "groupLevel": product.subscriptions?.groupLevel ?? 1, + //"availableInAllTerritories": product.territories.availableInAllTerritories + ], + "id": iapId, + "type": "subscriptions" + ] + ] + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(SubscriptionUpdateRequest.self, from: json) + let request = APIEndpoint.v1.subscriptions.id(iapId).patch(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("修改创建订阅商品失败: \(error.localizedDescription)") + } + return nil + } + + + /// 获取订阅商品本地化信息 + /// - Parameters: + /// - iapId: 内购商品 id + /// - Returns: 成功时返回对应的所有商品本地化信息 + func fetchSubscriptionLocalizations(iapId: String) async -> [ASCSubscriptionLocalization] { + let request = APIEndpoint.v1.subscriptions.id(iapId).subscriptionLocalizations + .get(parameters: .init( + limit: 200 + )) + do { + guard let provider = provider else { + return [] + } + var allApps: [ASCSubscriptionLocalization] = [] + for try await pagedResult in provider.paged(request) { + allApps.append(contentsOf: pagedResult.data) + } + return allApps + } catch { + handleError("获取订阅商品本地化信息失败: \(error.localizedDescription)") + return [] + } + + } + + + /// 创建订阅商品本地化 + /// - Parameters: + /// - iapId: 内购商品 id + /// - localization: 本地化信息模型 + /// - Returns: 成功时返回对应的本地化信息 + func createSubscriptionLocalization(iapId: String, localization: IAPLocalization) async -> ASCSubscriptionLocalization? { + let body = [ + "data": [ + "attributes": [ + "name": localization.name, + "description": localization.description, + "locale": localization.locale + ], + "relationships": [ + "subscription": [ + "data": [ + "id": iapId, + "type": "subscriptions" + ] + ] + ], + "type": "subscriptionLocalizations" + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(SubscriptionLocalizationCreateRequest.self, from: json) + let request = APIEndpoint.v1.subscriptionLocalizations.post(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("创建订阅商品本地化信息失败:\(error.localizedDescription)") + } + return nil + } + + + /// 修改订阅商品本地化信息 + /// - Parameters: + /// - iapLocaleId: 内购商品本地化语言 id + /// - localization: 内购商品本地化模型 + /// - product: 内购商品信息 + /// - Returns: 成功时返回对应的本地化信息 + func updateSubscriptionLocalization(iapLocaleId: String, localization: IAPLocalization) async -> ASCSubscriptionLocalization? { + let body = [ + "data": [ + "attributes": [ + "name": localization.name, + "description": localization.description, + ], + "id": iapLocaleId, + "type": "subscriptionLocalizations" + ] + ] + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(SubscriptionLocalizationUpdateRequest.self, from: json) + let request = APIEndpoint.v1.subscriptionLocalizations.id(iapLocaleId).patch(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("修改订阅商品本地化信息失败: \(error.localizedDescription)") + } + return nil + } + + + /// 获取订阅商品价格档位 + /// - Parameters: + /// - iapId: 内购商品 id + /// - territory: 国家或地区 + /// - Returns: 成功时返回对应的档位信息 + func fetchSubscriptionPricePoints(iapId: String, territory: [String]?) async -> [ASCSubscriptionPricePoint] { + let request = APIEndpoint.v1.subscriptions.id(iapId).pricePoints + .get(parameters: .init( + filterTerritory: territory, + limit: 200, + include: [.territory] + )) + do { + guard let provider = provider else { + return [] + } + var allApps: [ASCSubscriptionPricePoint] = [] + for try await pagedResult in provider.paged(request) { + allApps.append(contentsOf: pagedResult.data) + } + return allApps + } catch { + handleError("获取订阅商品价格档位失败: \(error.localizedDescription)") + return [] + } + } + + + /// 获取订阅商品价格档位的等价价格 + /// - Parameters: + /// - pointId: 内购价格档位 id + /// - territory: 国家或地区 + /// - Returns: 成功时返回对应的档位信息 + func fetchSubscriptionPricePointsEqualizations(pointId: String, territory: [String]?) async -> [ASCSubscriptionPricePoint] { + let request = APIEndpoint.v1.subscriptionPricePoints.id(pointId).equalizations + .get(parameters: .init( + filterTerritory: territory, + limit: 200, + include: [.territory] + )) + do { + guard let provider = provider else { + return [] + } + var allApps: [ASCSubscriptionPricePoint] = [] + for try await pagedResult in provider.paged(request) { + allApps.append(contentsOf: pagedResult.data) + } + return allApps + } catch { + handleError("获取订阅商品价格档位的等价价格失败: \(error.localizedDescription)") + return [] + } + } + + + /// 修改订阅商品价格档位 + /// - Parameters: + /// - iapId: 内购商品 id + /// - priceTierId: 价格档位标识(自定义名字) + /// - pricePointId: 价格档位 id + /// - Returns: 成功时返回对应的档位信息 + func updateSubscriptionPricePoint(iapId: String, pricePointId: String, preserveCurrentPrice: Bool) async -> ASCSubscriptionPrice? { + let body: [String : Any] = [ + "data": [ + "type": "subscriptionPrices", + "attributes": [ + "startDate": nil, + "preserveCurrentPrice": preserveCurrentPrice + ], + "relationships": [ + "subscription": [ + "data": [ + "id": iapId, + "type": "subscriptions" + ] + ], + "subscriptionPricePoint": [ + "data": [ + "id": pricePointId, + "type": "subscriptionPricePoints" + ] + ] + ] + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(SubscriptionPriceCreateRequest.self, from: json) + let request = APIEndpoint.v1.subscriptionPrices.post(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("修改订阅商品价格档位失败: \(error.localizedDescription)") + } + return nil + } + + + /// 获取订阅商品的送审截屏 + func fetchSubscriptionScreenshot(iapId: String) async -> ASCSubscriptionScreenshot? { + let request = APIEndpoint.v1.subscriptions.id(iapId).appStoreReviewScreenshot.get() + do { + guard let provider = provider else { + return nil + } + let shot = try await provider.request(request).data + return shot + } catch { + handleError("获取订阅商品的送审截图异常: \(error.localizedDescription)") + return nil + } + } + + /// 创建订阅商品的送审截屏 + func createSubscriptionScreenshot(iapId: String, fileName: String, fileSize: Int) async -> ASCSubscriptionScreenshot? { + let body = [ + "data": [ + "attributes": [ + "fileName": fileName, + "fileSize": fileSize, + ], + "relationships": [ + "subscription": [ + "data": [ + "id": iapId, + "type": "subscriptions" + ] + ] + ], + "type": "subscriptionAppStoreReviewScreenshots" + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(SubscriptionAppStoreReviewScreenshotCreateRequest.self, from: json) + let request = APIEndpoint.v1.subscriptionAppStoreReviewScreenshots.post(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("创建订阅商品的送审截图失败:\(error.localizedDescription)") + } + return nil + } + + + /// 确认订阅商品的送审截屏 + /// - Parameters: + /// - iapShotId: 内购商品截屏 id + /// - fileMD5: 内购截屏文件 md5 值 + /// - Returns: 成功时返回对应的截屏模型信息 + func updateSubscriptionScreenshot(iapShotId: String, fileMD5: String) async -> ASCSubscriptionScreenshot? { + let body = [ + "data": [ + "attributes": [ + "uploaded": true, + "sourceFileChecksum": fileMD5, + ], + "id": iapShotId, + "type": "subscriptionAppStoreReviewScreenshots" + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(SubscriptionAppStoreReviewScreenshotUpdateRequest.self, from: json) + let request = APIEndpoint.v1.subscriptionAppStoreReviewScreenshots.id(iapShotId).patch(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("修改订阅商品的送审截图失败: \(error.localizedDescription)") + } + return nil + } + + /// 删除订阅商品的送审截屏 + /// - Parameters: + /// - iapShotId: 内购商品截屏 id + /// - Returns: 成功时返回对应的截屏模型信息 + func deleteSubscriptionScreenshot(iapShotId: String) async -> Int { + do { + guard let provider = provider else { + return 400 + } + let request = APIEndpoint.v1.subscriptionAppStoreReviewScreenshots.id(iapShotId).delete + try await provider.request(request) + return 204 + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + return statusCode + } catch { + handleError("删除订阅商品的送审截图失败:\(error.localizedDescription)") + } + return 400 + } + + + /// 修改订阅商品的销售范围 + /// - Parameters: + /// - iapId: 内购商品 id + /// - availableTerritories: 可供销售的国家和地区列表 + /// - availableInNewTerritories: 是否在将来的所有 App Store 国家或地区中自动提供 App 内购买 + /// - Returns: 成功时返回对应的模型 + func updateSubscriptionAvailabilityTerritories(iapId: String, availableTerritories: [Any], availableInNewTerritories: Bool) async -> ASCSubscriptionAvailability? { + let body = [ + "data": [ + "type": "subscriptionAvailabilities", + "attributes": [ + "availableInNewTerritories": availableInNewTerritories + ], + "relationships": [ + "availableTerritories": [ + "data": availableTerritories +// [ +// [ +// "type": "territories", +// "id": "CHN" +// ] +// ] + ], + "subscription": [ + "data": [ + "id": iapId, + "type": "subscriptions" + ] + ] + ] + ] + ] + + do { + guard let provider = provider else { + return nil + } + let json = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + let model = try JSONDecoder().decode(SubscriptionAvailabilityCreateRequest.self, from: json) + let request = APIEndpoint.v1.subscriptionAvailabilities.post(model) + let data = try await provider.request(request).data + return data + } catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) { + handleRequestFailure(statusCode, errorResponse) + } catch { + handleError("修改订阅商品的销售范围失败: \(error.localizedDescription)") + } + return nil + } +} + + +// MARK: - handle log +extension APASCAPI { + + func handleRequestFailure(_ statusCode: Int, _ errorResponse: ErrorResponse?) { + print("Request failed with statuscode: \(statusCode) and the following errors:") + errorResponse?.errors?.forEach({ error in + handleError("Error code: \(error.code), title: \(error.title), detail: \(String(describing: error.detail))") + }) + } + + func handleError(_ msg: String) { + addMessage(msg) + print(msg) + } + + func addMessage(_ msg: String) { + let dateFormatter : DateFormatter = DateFormatter() + dateFormatter.dateFormat = "[MM-dd HH:mm:ss] " + let currentDateString = dateFormatter.string(from: Date()) + let log = currentDateString + msg + message.append(log) + + // 本地保存 + saveLogs(log: log) + } + + func saveLogs(log: String, retry: Int = 3) { + do { + try log.appendLine(to: logPath.createFilePath) + } catch { + if retry > 0 { + self.addMessage("‼️ retry save logs file~ error:\(error)") + saveLogs(log: log, retry: retry - 1) + } + } + } +} diff --git a/AppleParty/AppleParty/Shared/UI/APASCKeysEditVC.swift b/AppleParty/AppleParty/Shared/UI/APASCKeysEditVC.swift new file mode 100644 index 0000000..473f08d --- /dev/null +++ b/AppleParty/AppleParty/Shared/UI/APASCKeysEditVC.swift @@ -0,0 +1,95 @@ +// +// APSPasswordEditVC.swift +// AppleParty +// +// Created by HTC on 2022/5/18. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APASCKeysEditVC: NSViewController { + + @IBOutlet weak var titleLbl: NSTextField! + @IBOutlet var accountTextView: NSTextField! + @IBOutlet var issuerIDTextView: NSTextField! + @IBOutlet var privateKeyIDTextView: NSTextField! + @IBOutlet var privateKeyTextView: NSTextField! + @IBOutlet weak var usePasswordBtn: NSButton! + + public var titleString: String? + public var spassword: AppStoreConnectKey? + public var updateCompletion: ((_ model: AppStoreConnectKey) -> Void)? + + + override func viewDidLoad() { + super.viewDidLoad() + + if let text = titleString { + titleLbl.stringValue = text + } + + if let model = spassword { + accountTextView.stringValue = model.aliasName + issuerIDTextView.stringValue = model.issuerID + privateKeyIDTextView.stringValue = model.privateKeyID + privateKeyTextView.stringValue = model.privateKey + usePasswordBtn.state = model.isused ? .on : .off + } else { + // 新建时,默认读取当前开发者名称 + accountTextView.stringValue = UserCenter.shared.developerName + } + } + + @IBAction func clickedInputFileBtn(_ sender: Any) { + let openPanel = NSOpenPanel() + openPanel.canChooseFiles = true + openPanel.canChooseDirectories = false + openPanel.allowsMultipleSelection = false + openPanel.allowedFileTypes = ["p8", "P8"] + + openPanel.beginSheetModal(for: self.view.window!) { (modalResponse) in + if modalResponse == .OK { + if let fileURL = openPanel.url { + self.handleP8fileContent(fileURL) + } + } + } + } + + @IBAction func clickedCancelBtn(_ sender: Any) { + dismiss(self) + } + + @IBAction func clickedSaveBtn(_ sender: Any) { + let account = accountTextView.stringValue.trim() + let issuerID = issuerIDTextView.stringValue.trim() + let privateKeyID = privateKeyIDTextView.stringValue.trim() + let privateKey = privateKeyTextView.stringValue.trim() + + guard account.isNotEmpty, issuerID.isNotEmpty, privateKeyID.isNotEmpty, privateKey.isNotEmpty else { + APHUD.hide(message: "所有配置项不能为空!", view: view, delayTime: 1) + return + } + + if let block = updateCompletion { + block(AppStoreConnectKey(aliasName: account, issuerID: issuerID, privateKeyID: privateKeyID, privateKey: privateKey, isused: usePasswordBtn.state == .on)) + } + dismiss(self) + } + + func handleP8fileContent(_ url: URL) { + guard let data = try? Data(contentsOf: url), + var content = String(data: data, encoding: .utf8) else { + APHUD.hide(message: "p8文件内容读取失败!请检查文件是否完整!", delayTime: 1) + return + } + let begin = "-----BEGIN PRIVATE KEY-----" + let end = "-----END PRIVATE KEY-----" + content = content.replacingOccurrences(of: begin, with: "") + content = content.replacingOccurrences(of: end, with: "") + content = content.replacingOccurrences(of: "\n", with: "") + privateKeyTextView.stringValue = content + } + +} diff --git a/AppleParty/AppleParty/Shared/UI/APASCKeysEditVC.xib b/AppleParty/AppleParty/Shared/UI/APASCKeysEditVC.xib new file mode 100644 index 0000000..8b72d4d --- /dev/null +++ b/AppleParty/AppleParty/Shared/UI/APASCKeysEditVC.xib @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/Shared/UI/APASCKeysSettingVC.swift b/AppleParty/AppleParty/Shared/UI/APASCKeysSettingVC.swift new file mode 100644 index 0000000..02d2535 --- /dev/null +++ b/AppleParty/AppleParty/Shared/UI/APASCKeysSettingVC.swift @@ -0,0 +1,189 @@ +// +// APASCKeysSettingVC.swift +// AppleParty +// +// Created by HTC on 2022/11/18. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APASCKeysSettingVC: NSViewController { + + // 模型 + var models = [AppStoreConnectKey]() + // 回调当前选择的账号 + var updateCompletion: ((_ model: AppStoreConnectKey?) -> Void)? + + @IBOutlet weak var tableView: NSTableView! + + + @IBAction func clickedAddBtn(_ sender: Any) { + let vc = APASCKeysEditVC() + vc.titleString = "新增 API 密钥" + vc.updateCompletion = { [weak self] news in + // 相同账号的只保留最新 + self?.models = self?.models.filter({ $0.aliasName != news.aliasName }) ?? [] + if news.isused { + // 只能有一个是使用的账号,其它为否 + self?.models = self?.models.map({ sp in + var spp = sp + return spp.model(sp, false) + }) ?? [] + } + self?.models.append(news) + self?.tableView.reloadData() + } + presentAsSheet(vc) + } + + @IBAction func clickedSaveBtn(_ sender: Any) { + + guard models.isNotEmpty else { + APHUD.hide(message: "密钥配置不能为空!", view: view, delayTime: 2) + return + } + + // 保存数据 + InfoCenter.shared.ascKeys = models + + // 回调当前选择的账号 + if let block = updateCompletion { + let models = self.models.filter({ $0.isused == true }) + block(models.first) + } + dismiss(self) + } + + @IBAction func clickedCancelBtn(_ sender: Any) { + dismiss(self) + } + + private lazy var editMenu: NSMenu = { + let menu = NSMenu() + let saveItem = NSMenuItem() + saveItem.title = "修改" + saveItem.target = self + saveItem.action = #selector(tableViewEditItemClicked) + menu.addItem(saveItem) + let removeItem = NSMenuItem() + removeItem.title = "删除" + removeItem.target = self + removeItem.action = #selector(tableViewDeleteItemClicked) + menu.addItem(removeItem) + return menu + }() + + @objc private func tableViewEditItemClicked(_ sender: AnyObject) { + let row = tableView.clickedRow + guard row >= 0 else { return } + + let result = models + let index = result.index(result.startIndex, offsetBy: row) + let model = result[index] + let vc = APASCKeysEditVC() + vc.titleString = "修改 API 密钥" + vc.spassword = model + vc.updateCompletion = { [weak self] news in + if news.isused { + // 只能有一个是使用的账号,其它为否 + self?.models = self?.models.map({ sp in + var spp = sp + return spp.model(sp, false) + }) ?? [] + } + self?.models[index] = news + self?.tableView.reloadData() + } + presentAsSheet(vc) + } + + @objc private func tableViewDeleteItemClicked(_ sender: AnyObject) { + let row = tableView.clickedRow + guard row >= 0 else { return } + + let result = models + let index = result.index(result.startIndex, offsetBy: row) + models.remove(at: index) + tableView.reloadData() + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + func setupUI() { + + tableView.menu = editMenu + tableView.delegate = self + tableView.dataSource = self + + models = InfoCenter.shared.ascKeys + tableView.reloadData() + } +} + + +// MARK: NSTableViewDataSource && NSTableViewDelegate +extension APASCKeysSettingVC: NSTableViewDataSource, NSTableViewDelegate { + func numberOfRows(in tableView: NSTableView) -> Int { + return models.count + } + + func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { + return 30.0 + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + let result = models + let index = result.index(result.startIndex, offsetBy: row) + let model = result[index] + let identifier = tableColumn!.identifier + let identifierString = identifier.rawValue + + if identifierString == "AccountCell" { + let cellView = tableView.makeView(withIdentifier: identifier, owner: self) as! NSTableCellView + cellView.textField!.stringValue = model.aliasName + return cellView + } + else if identifierString == "IssuerIDCell" { + let cellView = tableView.makeView(withIdentifier: identifier, owner: self) as! NSTableCellView + cellView.textField!.stringValue = model.issuerID + return cellView + } + else if identifierString == "currentUseCell" { + let cellView = tableView.makeView(withIdentifier: identifier, owner: self) as! NSTableCellView + cellView.textField!.stringValue = model.isused ? "✓" : "-" + return cellView + } + else if identifierString == "PrivateKeyID" { + let cellView = tableView.makeView(withIdentifier: identifier, owner: self) as! NSTableCellView + cellView.textField!.stringValue = model.privateKeyID + return cellView + } + else if identifierString == "PrivateKey" { + let cellView = tableView.makeView(withIdentifier: identifier, owner: self) as! NSTableCellView + cellView.textField!.stringValue = model.privateKey + return cellView + } + else { + print("unhandled colum id: \(identifierString)") + } + return nil + } + + + // MARK: 是否可以选中单元格 + func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { + + return true + } + + func tableViewSelectionDidChange(_ notification: Notification) { + let table = notification.object as! NSTableView + table.deselectRow(table.selectedRow) + } + +} + diff --git a/AppleParty/AppleParty/Shared/UI/APASCKeysSettingVC.xib b/AppleParty/AppleParty/Shared/UI/APASCKeysSettingVC.xib new file mode 100644 index 0000000..2dea352 --- /dev/null +++ b/AppleParty/AppleParty/Shared/UI/APASCKeysSettingVC.xib @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/Shared/UI/APCollectionView.swift b/AppleParty/AppleParty/Shared/UI/APCollectionView.swift new file mode 100644 index 0000000..59105ec --- /dev/null +++ b/AppleParty/AppleParty/Shared/UI/APCollectionView.swift @@ -0,0 +1,79 @@ +// +// APCollectionView.swift +// AppleParty +// +// Created by HTC on 2022/3/14. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APCollectionView: NSView { + + lazy var scrollView: NSScrollView = { + let scrollView = NSScrollView() + let margin: CGFloat = 20 + scrollView.automaticallyAdjustsContentInsets = false + scrollView.contentInsets = NSEdgeInsetsMake(0, margin, 0, margin) + scrollView.scrollerInsets = NSEdgeInsetsMake(0, 0, 0, -margin) + return scrollView + }() + + lazy var collectionView: NSCollectionView = { + let itemWidth = CGFloat(150.0) + let itemHeight = CGFloat(150.0) + let itemSpacing = CGFloat(100.0) + let itemPadding = CGFloat(50.0) + + let flowLayout = NSCollectionViewFlowLayout() + flowLayout.scrollDirection = .vertical + flowLayout.itemSize = NSMakeSize(itemWidth, itemHeight) + flowLayout.minimumInteritemSpacing = itemSpacing + flowLayout.minimumLineSpacing = itemSpacing + flowLayout.sectionInset = NSEdgeInsetsMake(itemPadding, itemPadding, itemPadding, itemPadding) + + let collection = NSCollectionView() + collection.collectionViewLayout = flowLayout + collection.isSelectable = true + return collection + }() + + init() { + super.init(frame: .zero) + addSubviews() + addConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension APCollectionView { + + func configure(superView: NSView) { + superView.addSubview(self) + self.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + self.topAnchor.constraint(equalTo: superView.topAnchor), + self.leadingAnchor.constraint(equalTo: superView.leadingAnchor), + self.trailingAnchor.constraint(equalTo: superView.trailingAnchor), + self.bottomAnchor.constraint(equalTo: superView.bottomAnchor) + ]) + } + + private func addSubviews() { + scrollView.documentView = collectionView + [scrollView].forEach(addSubview) + } + + private func addConstraints() { + scrollView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: scrollView.superview!.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: scrollView.superview!.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: scrollView.superview!.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: scrollView.superview!.bottomAnchor) + ]) + } +} diff --git a/AppleParty/AppleParty/Shared/UI/APDebugVC.storyboard b/AppleParty/AppleParty/Shared/UI/APDebugVC.storyboard new file mode 100644 index 0000000..6d97178 --- /dev/null +++ b/AppleParty/AppleParty/Shared/UI/APDebugVC.storyboard @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/Shared/UI/APDebugVC.swift b/AppleParty/AppleParty/Shared/UI/APDebugVC.swift new file mode 100644 index 0000000..cc65116 --- /dev/null +++ b/AppleParty/AppleParty/Shared/UI/APDebugVC.swift @@ -0,0 +1,140 @@ +// +// APDebugVC.swift +// AppleParty +// +// Created by HTC on 2022/3/21. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APDebugVC: NSViewController { + + @IBOutlet var debugTextView: NSTextView! + @IBOutlet weak var refreshBtn: NSButton! + @IBOutlet weak var shareBtn: NSButton! + + var debugLog: String = "" { + didSet { + uploadData() + } + } + + var fileURL: URL? { + didSet { + refreshBtn.isHidden = false + reloadFileLogs() + } + } + + // Menu + private lazy var editMenu: NSMenu = { + let menu = NSMenu() + let editMenuItems = [ + NSMenuItem(title: "邮件发送", action: #selector(emailShare), keyEquivalent: ""), + NSMenuItem(title: "隔空投送", action: #selector(airDropShare), keyEquivalent: ""), + NSMenuItem(title: "其它方式", action: #selector(otherShare), keyEquivalent: ""), + ] + for editMenuItem in editMenuItems { + menu.addItem(editMenuItem) + } + return menu + }() + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func clickedRefreshBtn(_ sender: Any) { + reloadFileLogs() + } + + @IBAction func clickedShareBtn(_ sender: NSButton) { + let p = NSPoint(x: sender.frame.width, y: 0) //按钮右边 + self.editMenu.popUp(positioning: nil, at: p, in: sender) + } + +} + + +extension APDebugVC { + + func uploadData() { + DispatchQueue.main.async { + self.debugTextView.string = self.debugLog + self.debugTextView.scrollRangeToVisible(NSMakeRange(self.debugTextView.string.count, 0)) + } + } + + + func reloadFileLogs() { + guard let file = fileURL, let logs = try? String(contentsOf: file, encoding: .utf8) else { + debugTextView.string = "读取日志失败!\(String(describing: fileURL?.path))" + return + } + debugLog = logs + } + + func getTextFileURL(text: String) -> URL? { + + guard let data = text.data(using: .utf8) else { return nil } + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMdd_HHmm" + let date = dateFormatter.string(from: Date()) + let filename = "AppleParty-Logs_\(date).txt" + + let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent(filename) + + do { + try data.write(to: tempURL, options: .atomic) + + return tempURL + } catch { + assertionFailure("Failed to write temporary URL for pasteboard: \(String(describing: error))") + return nil + } + } +} + + +extension APDebugVC { + + @objc func emailShare() { + let text = debugTextView.string + let mainStoryBoard = NSStoryboard(name: "EmailTool", bundle: nil) + let windowController = mainStoryBoard.instantiateController(withIdentifier: "EmailTool") as! NSWindowController + let controller = windowController.contentViewController as! EmailToolVC + controller.emailTitle = "苹果派-错误日志" + controller.emailContent = text + controller.attachmentFileUrl = getTextFileURL(text: text) + windowController.showWindow(self) + } + + @objc func airDropShare() { + let text = debugTextView.string + guard let url = getTextFileURL(text: text) else { + otherShare() + return + } + + let service = NSSharingService(named: .sendViaAirDrop)! + let items: [NSURL] = [url as NSURL] + if service.canPerform(withItems: items) { + service.perform(withItems: items) + } else { + NSAlert.show("Cannot perform AirDrop!") + } + } + + @objc func otherShare() { + var item: Any = debugTextView.string + if let text = item as? String, let url = getTextFileURL(text: text) { + item = url + } + let picker = NSSharingServicePicker(items: [item]) + picker.show(relativeTo: .zero, of: shareBtn, preferredEdge: .maxX) + } + +} diff --git a/AppleParty/AppleParty/Shared/UI/APSPasswordEditVC.swift b/AppleParty/AppleParty/Shared/UI/APSPasswordEditVC.swift new file mode 100644 index 0000000..1655fde --- /dev/null +++ b/AppleParty/AppleParty/Shared/UI/APSPasswordEditVC.swift @@ -0,0 +1,59 @@ +// +// APSPasswordEditVC.swift +// AppleParty +// +// Created by HTC on 2022/5/18. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APSPasswordEditVC: NSViewController { + + @IBOutlet weak var titleLbl: NSTextField! + @IBOutlet var accountTextView: NSTextField! + @IBOutlet var passwordTextView: NSTextField! + @IBOutlet weak var usePasswordBtn: NSButton! + + public var titleString: String? + public var spassword: SPassword? + public var updateCompletion: ((_ model: SPassword) -> Void)? + + + override func viewDidLoad() { + super.viewDidLoad() + + if let text = titleString { + titleLbl.stringValue = text + } + + if let model = spassword { + accountTextView.stringValue = model.account + passwordTextView.stringValue = model.password + usePasswordBtn.state = model.isused ? .on : .off + } else { + // 新建时,默认读取当前账号的邮件名 + accountTextView.stringValue = UserCenter.shared.loginedUser.appleid + } + } + + @IBAction func clickedCancelBtn(_ sender: Any) { + dismiss(self) + } + + @IBAction func clickedSaveBtn(_ sender: Any) { + let account = accountTextView.stringValue.trim() + let password = passwordTextView.stringValue.trim() + + guard account.isNotEmpty, password.isNotEmpty else { + APHUD.hide(message: "账号邮箱和专用密码不能为空!", view: view, delayTime: 1) + return + } + + if let block = updateCompletion { + block(SPassword(account: account, password: password, isused: usePasswordBtn.state == .on)) + } + dismiss(self) + } + +} diff --git a/AppleParty/AppleParty/Shared/UI/APSPasswordEditVC.xib b/AppleParty/AppleParty/Shared/UI/APSPasswordEditVC.xib new file mode 100644 index 0000000..257c382 --- /dev/null +++ b/AppleParty/AppleParty/Shared/UI/APSPasswordEditVC.xib @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/Shared/UI/APSPasswordSettingVC.swift b/AppleParty/AppleParty/Shared/UI/APSPasswordSettingVC.swift new file mode 100644 index 0000000..c9672a6 --- /dev/null +++ b/AppleParty/AppleParty/Shared/UI/APSPasswordSettingVC.swift @@ -0,0 +1,178 @@ +// +// APSPasswordSettingVC.swift +// AppleParty +// +// Created by HTC on 2022/5/18. +// Copyright © 2022 37 Mobile models. All rights reserved. +// + +import Cocoa + +class APSPasswordSettingVC: NSViewController { + + // 模型 + var models = [SPassword]() + // 回调当前选择的账号 + var updateCompletion: ((_ model: SPassword?) -> Void)? + + @IBOutlet weak var tableView: NSTableView! + + + @IBAction func clickedAddBtn(_ sender: Any) { + let vc = APSPasswordEditVC() + vc.titleString = "新增专用密码" + vc.updateCompletion = { [weak self] news in + // 相同账号的只保留最新 + self?.models = self?.models.filter({ $0.account != news.account }) ?? [] + if news.isused { + // 只能有一个是使用的账号,其它为否 + self?.models = self?.models.map({ sp in + var spp = sp + return spp.model(sp, false) + }) ?? [] + } + self?.models.append(news) + self?.tableView.reloadData() + } + presentAsSheet(vc) + } + + @IBAction func clickedSaveBtn(_ sender: Any) { + + guard models.isNotEmpty else { + APHUD.hide(message: "账号邮箱和专用密码不能为空!", view: view, delayTime: 2) + return + } + + // 保存数据 + UserCenter.shared.secondaryPasswordList = models + + // 回调当前选择的账号 + if let block = updateCompletion { + let models = self.models.filter({ $0.isused == true }) + block(models.first) + } + dismiss(self) + } + + @IBAction func clickedCancelBtn(_ sender: Any) { + dismiss(self) + } + + private lazy var editMenu: NSMenu = { + let menu = NSMenu() + let saveItem = NSMenuItem() + saveItem.title = "修改" + saveItem.target = self + saveItem.action = #selector(tableViewEditItemClicked) + menu.addItem(saveItem) + let removeItem = NSMenuItem() + removeItem.title = "删除" + removeItem.target = self + removeItem.action = #selector(tableViewDeleteItemClicked) + menu.addItem(removeItem) + return menu + }() + + @objc private func tableViewEditItemClicked(_ sender: AnyObject) { + let row = tableView.clickedRow + guard row >= 0 else { return } + + let result = models + let index = result.index(result.startIndex, offsetBy: row) + let model = result[index] + let vc = APSPasswordEditVC() + vc.titleString = "新增专用密码" + vc.spassword = model + vc.updateCompletion = { [weak self] news in + if news.isused { + // 只能有一个是使用的账号,其它为否 + self?.models = self?.models.map({ sp in + var spp = sp + return spp.model(sp, false) + }) ?? [] + } + self?.models[index] = news + self?.tableView.reloadData() + } + presentAsSheet(vc) + } + + @objc private func tableViewDeleteItemClicked(_ sender: AnyObject) { + let row = tableView.clickedRow + guard row >= 0 else { return } + + let result = models + let index = result.index(result.startIndex, offsetBy: row) + models.remove(at: index) + tableView.reloadData() + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + func setupUI() { + + tableView.menu = editMenu + tableView.delegate = self + tableView.dataSource = self + + models = UserCenter.shared.secondaryPasswordList + tableView.reloadData() + } +} + + +// MARK: NSTableViewDataSource && NSTableViewDelegate +extension APSPasswordSettingVC: NSTableViewDataSource, NSTableViewDelegate { + func numberOfRows(in tableView: NSTableView) -> Int { + return models.count + } + + func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { + return 30.0 + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + let result = models + let index = result.index(result.startIndex, offsetBy: row) + let model = result[index] + let identifier = tableColumn!.identifier + let identifierString = identifier.rawValue + + if identifierString == "AccountCell" { + let cellView = tableView.makeView(withIdentifier: identifier, owner: self) as! NSTableCellView + cellView.textField!.stringValue = model.account + return cellView + } + else if identifierString == "PasswordCell" { + let cellView = tableView.makeView(withIdentifier: identifier, owner: self) as! NSTableCellView + cellView.textField!.stringValue = model.password + return cellView + } + else if identifierString == "currentUseCell" { + let cellView = tableView.makeView(withIdentifier: identifier, owner: self) as! NSTableCellView + cellView.textField!.stringValue = model.isused ? "✓" : "-" + return cellView + } + else { + print("unhandled colum id: \(identifierString)") + } + return nil + } + + + // MARK: 是否可以选中单元格 + func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { + + return true + } + + func tableViewSelectionDidChange(_ notification: Notification) { + let table = notification.object as! NSTableView + table.deselectRow(table.selectedRow) + } + +} diff --git a/AppleParty/AppleParty/Shared/UI/APSPasswordSettingVC.xib b/AppleParty/AppleParty/Shared/UI/APSPasswordSettingVC.xib new file mode 100644 index 0000000..3dbc215 --- /dev/null +++ b/AppleParty/AppleParty/Shared/UI/APSPasswordSettingVC.xib @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/Shared/UI/DropZoneView.swift b/AppleParty/AppleParty/Shared/UI/DropZoneView.swift new file mode 100644 index 0000000..07f015e --- /dev/null +++ b/AppleParty/AppleParty/Shared/UI/DropZoneView.swift @@ -0,0 +1,316 @@ +// +// DropZoneView.swift +// SymbolicatorX +// +// Created by 钟晓跃 on 2020/7/6. +// Copyright © 2020 钟晓跃. All rights reserved. +// + +import Cocoa +import SnapKit + +protocol DropZoneViewDelegate: AnyObject { + func receivedFile(dropZoneView: DropZoneView, fileURL: URL) + func receivedMouseDown(dropZoneView: DropZoneView, theEvent: NSEvent) + func receivedRightMouseDown(dropZoneView: DropZoneView, theEvent: NSEvent) +} + +// @optional +extension DropZoneViewDelegate { + func receivedMouseDown(dropZoneView: DropZoneView, theEvent: NSEvent) {} + func receivedRightMouseDown(dropZoneView: DropZoneView, theEvent: NSEvent) {} +} + +class DropZoneView: NSView { + + private let containerView = NSView() + private let iconImageView = NSImageView() + private let fileTypeTextField = NSTextField() + private let textTextField = NSTextField() + private let detailTextTextField = NSTextField() + + weak var delegate: DropZoneViewDelegate? + private var isHoveringFile = false + private var fileTypes = [String]() + private var file: URL? + + private var defaultText: String? + private var defaultDetailText: String? + + var fileTypesPredicate: NSPredicate { + let predicateFormat = (0.. String in + return "SELF ENDSWITH[c] %@" + }.joined(separator: " OR ") + return NSPredicate(format: predicateFormat, argumentArray: fileTypes) + } + + init(fileTypes: [String], text: String? = nil, detailText: String? = nil) { + super.init(frame: .zero) + defaultText = text + defaultDetailText = detailText + setFileTypes(fileTypes) + setText(text) + setDetailText(detailText) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func reset() { + setFile(nil) + setDetailText(nil) + setText(defaultText) + setDetailText(defaultDetailText) + } + + public func setHoveringFile(_ isHoveringFile: Bool) { + + self.isHoveringFile = isHoveringFile + display() + } + + public func setIcon(_ icon: NSImage?) { + + icon?.size = NSSize(width: 64, height: 64) + iconImageView.image = icon + iconImageView.sizeToFit() + } + + public func setFile(_ file: URL?) { + + guard file != self.file else { return } + self.file = file + setText(file?.lastPathComponent) + display() + } + + public func setText(_ text: String?) { + + guard let newText = text else { + textTextField.stringValue = "" + return + } + + textTextField.attributedStringValue = NSAttributedString(string: newText, attributes: Style.textAttributes(size: 14, color: .secondaryLabelColor)) + } + + public func setDetailText(_ detailText: String?) { + + guard let newDetailText = detailText else { + detailTextTextField.stringValue = "" + return + } + + detailTextTextField.attributedStringValue = NSAttributedString(string: newDetailText, attributes: Style.textAttributes(size: 12, color: .tertiaryLabelColor)) + } + + public func setFileTypes(_ fileTypes: [String]) { + +// guard !fileTypes.isEmpty else { +// setIcon(nil) +// fileTypeTextField.attributedStringValue = NSAttributedString() +// unregisterDraggedTypes() +// return +// } + + self.fileTypes = fileTypes.map({ (fileType) -> String in + return (fileType.hasPrefix(".") ? "" : ".").appending(fileType.lowercased()) + }) + + registerForDraggedTypes([(kUTTypeFileURL as + NSPasteboard.PasteboardType)]) + + let primaryFileType = self.fileTypes.isEmpty ? "" : fileTypes[0] + setIcon(NSWorkspace.shared.icon(forFileType: primaryFileType)) + fileTypeTextField.attributedStringValue = NSAttributedString(string: primaryFileType, attributes: Style.textAttributes(size: 16, color: .labelColor)) + } + + public func acceptFile(url fileURL: URL) -> Bool { + + guard validFileURL(fileURL) else { return false } + + setFile(fileURL) + delegate?.receivedFile(dropZoneView: self, fileURL: fileURL) + + return true + } +} + +// MARK: - NSDraggingDestination +extension DropZoneView { + override func mouseDown(with theEvent: NSEvent) { + delegate?.receivedMouseDown(dropZoneView: self, theEvent: theEvent) + } + + override func rightMouseDown(with theEvent: NSEvent) { + delegate?.receivedRightMouseDown(dropZoneView: self, theEvent: theEvent) + } + + override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { + + let isHoveringFile = (validDraggedFileURL(from: sender) != nil) + setHoveringFile(isHoveringFile) + return isHoveringFile ? .copy : [] + } + + override func draggingExited(_ sender: NSDraggingInfo?) { + + setHoveringFile(false) + } + + override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool { + + return true + } + + override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { + + defer { setHoveringFile(false) } + + guard let draggedFileURL = validDraggedFileURL(from: sender) else { return false } + + setFile(draggedFileURL) + delegate?.receivedFile(dropZoneView: self, fileURL: draggedFileURL) + + return true + } + + private func validDraggedFileURL(from draggingInfo: NSDraggingInfo) -> URL? { + + guard + let draggedFile = draggingInfo.draggingPasteboard.string(forType: kUTTypeFileURL as NSPasteboard.PasteboardType), + let draggedFileURL = URL(string: draggedFile) + else { + return nil + } + + return validFileURL(draggedFileURL) ? draggedFileURL : nil + } + + private func validFileURL(_ url: URL) -> Bool { + if fileTypes.isEmpty { + let fm = FileManager.default + var isDirectory: ObjCBool = false + // 不允许是目录 + if (fm.fileExists(atPath: url.path, isDirectory: &isDirectory) && isDirectory.boolValue) { + return false + } + return true + } + return fileTypesPredicate.evaluate(with: url.path) + } +} + +// MARK: - Helpers +extension DropZoneView { + + private struct Colors { + static let gray1 = NSColor(calibratedWhite: 0.7, alpha: 1) + static let gray2 = NSColor(calibratedWhite: 0.4, alpha: 1) + static let shade = NSColor(calibratedWhite: 0.0, alpha: 0.025) + } + + private struct Style { + + static func textAttributes(size: CGFloat, color: NSColor) -> [NSAttributedString.Key: Any] { + + let centeredTextStyle = (NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle) + centeredTextStyle.alignment = .center + + return [ + NSAttributedString.Key.font: NSFont.systemFont(ofSize: size), + NSAttributedString.Key.foregroundColor: color, + NSAttributedString.Key.paragraphStyle: centeredTextStyle + ] + } + } +} + +// MARK: - UI +extension DropZoneView { + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + (isHoveringFile ? Colors.shade : NSColor.clear).setFill() + dirtyRect.fill() + + let borderPadding: CGFloat = 6 + let drawRect = dirtyRect.insetBy(dx: borderPadding, dy: borderPadding) + + let alpha: CGFloat = file == nil ? 1 : 0.2 + let dashed = file == nil + + (isHoveringFile ? Colors.gray2 : Colors.gray1).withAlphaComponent(alpha).setStroke() + + let roundedRectanglePath = NSBezierPath(roundedRect: drawRect, xRadius: 8, yRadius: 8) + roundedRectanglePath.lineWidth = 1.5 + if dashed { + roundedRectanglePath.setLineDash([6, 6, 6, 6], count: 4, phase: 0) + } + roundedRectanglePath.stroke() + } + + private func setupUI() { + + wantsLayer = true + layer?.cornerRadius = 14 + translatesAutoresizingMaskIntoConstraints = false + + addSubview(containerView) + containerView.snp.makeConstraints { (make) in + make.center.equalTo(self) + make.width.equalTo(self).offset(-40) + } + + iconImageView.translatesAutoresizingMaskIntoConstraints = false + iconImageView.unregisterDraggedTypes() + containerView.addSubview(iconImageView) + iconImageView.snp.makeConstraints { (make) in + make.top.centerX.equalToSuperview() + make.size.equalTo(64) + } + + containerView.translatesAutoresizingMaskIntoConstraints = false + fileTypeTextField.drawsBackground = false + fileTypeTextField.isBezeled = false + fileTypeTextField.isEditable = false + fileTypeTextField.isSelectable = false + containerView.addSubview(fileTypeTextField) + fileTypeTextField.snp.makeConstraints { (make) in + make.centerX.equalToSuperview() + make.top.equalTo(iconImageView.snp.bottom).offset(4) + make.height.lessThanOrEqualTo(26) + } + + textTextField.translatesAutoresizingMaskIntoConstraints = false + textTextField.drawsBackground = false + textTextField.isBezeled = false + textTextField.isEditable = false + textTextField.isSelectable = false + textTextField.cell?.lineBreakMode = .byTruncatingMiddle + containerView.addSubview(textTextField) + textTextField.snp.makeConstraints { (make) in + make.centerX.equalToSuperview() + make.top.equalTo(fileTypeTextField.snp.bottom).offset(12) + make.width.equalToSuperview().multipliedBy(0.9) + make.height.lessThanOrEqualTo(70) + } + + detailTextTextField.translatesAutoresizingMaskIntoConstraints = false + detailTextTextField.drawsBackground = false + detailTextTextField.isBezeled = false + detailTextTextField.isEditable = false + detailTextTextField.isSelectable = false + detailTextTextField.cell?.truncatesLastVisibleLine = true + containerView.addSubview(detailTextTextField) + detailTextTextField.snp.makeConstraints { (make) in + make.centerX.bottom.equalToSuperview() + make.top.equalTo(textTextField.snp.bottom) + make.width.equalToSuperview().multipliedBy(0.9) + make.height.lessThanOrEqualTo(70) + } + } +} diff --git a/AppleParty/AppleParty/Shared/UI/UIExtension.swift b/AppleParty/AppleParty/Shared/UI/UIExtension.swift new file mode 100644 index 0000000..81ae907 --- /dev/null +++ b/AppleParty/AppleParty/Shared/UI/UIExtension.swift @@ -0,0 +1,43 @@ +// +// UIExtension.swift +// AppleParty +// +// Created by HTC on 2022/3/12. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import AppKit +import Foundation + + +extension NSAlert { + + static func show(_ content: String, title: String = "提示") { + let alert = NSAlert() + alert.messageText = title + alert.informativeText = content + alert.runModal() + } +} + + +extension NSImageView { + func showWebImage(_ url: URL) { + URLSession.shared.dataTask(with: url) { data, response, error in + guard + let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200, + let mimeType = response?.mimeType, mimeType.hasPrefix("image"), + let data = data, error == nil, + let image = NSImage(data: data) + else { return } + DispatchQueue.main.async() { [weak self] in + self?.image = image + } + }.resume() + } + + func showWebImage(_ link: String) { + guard let url = URL(string: link) else { return } + showWebImage(url) + } +} diff --git a/AppleParty/AppleParty/Shared/Utils/APHUD.swift b/AppleParty/AppleParty/Shared/Utils/APHUD.swift new file mode 100644 index 0000000..d5eefa5 --- /dev/null +++ b/AppleParty/AppleParty/Shared/Utils/APHUD.swift @@ -0,0 +1,64 @@ +// +// APHUD.swift +// AppleParty +// +// Created by HTC on 2022/3/18. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +let APHUD = HUD.shared + +class HUD: NSObject { + + static let shared = HUD() + + private var loadHud: MBProgressHUD? + private var textHud: MBProgressHUD? + + func showLoading(_ view: NSView = currentView()) { + if loadHud != nil { + loadHud?.hide(true) + } + guard let loadHud = MBProgressHUD(view: view) else { + return + } + self.loadHud = loadHud + view.addSubview(loadHud) + loadHud.show(true) + } + + func hideLoading() { + loadHud?.hide(true) + } + + func show(message: String, view: NSView = currentView()) { + if textHud != nil { + textHud?.hide(true) + } + guard let textHud = MBProgressHUD(view: view) else { + return + } + self.textHud = textHud + textHud.labelText = message + view.addSubview(textHud) + textHud.show(true) + } + + func hide() { + textHud?.hide(true) + } + + func hide(message: String, view: NSView = currentView(), delayTime: TimeInterval = 3) { + guard let hud = MBProgressHUD(view: view) else { + return + } + hud.mode = MBProgressHUDModeText + hud.labelText = message + hud.removeFromSuperViewOnHide = true + view.addSubview(hud) + hud.show(true) + hud.hide(true, afterDelay: delayTime) + } +} diff --git a/AppleParty/AppleParty/Shared/Utils/APUtil.swift b/AppleParty/AppleParty/Shared/Utils/APUtil.swift new file mode 100644 index 0000000..8f26d88 --- /dev/null +++ b/AppleParty/AppleParty/Shared/Utils/APUtil.swift @@ -0,0 +1,98 @@ +// +// APUtil.swift +// AppleParty +// +// Created by HTC on 2022/3/14. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Foundation +import KeychainAccess + + +public struct Environment { + public var keychain = APKeychain() + public var defaults = APDefaults() +} + +public var APUtil = Environment() + +/// refer: https://github.com/kishikawakatsumi/KeychainAccess +public struct APKeychain { + private static let keychain = KeychainAccess.Keychain(service: "com.37iOS.AppleParty") + + public func getString(_ key: String) throws -> String? { + try APKeychain.keychain.getString(key) + } + + public func set(_ value: String, key: String) throws { + try APKeychain.keychain.set(value, key: key) + } + + public func getData(_ key: String) throws -> Data? { + try APKeychain.keychain.getData(key) + } + + public func set(_ value: Data, key: String) throws { + try APKeychain.keychain.set(value, key: key) + } + + public func getDict(_ key: String) throws -> [String: String]? { + if let data = try APKeychain.keychain.getData(key) { + do { + let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:String] + return json + } catch { + print("APKeychain getDict something went wrong: \(error.localizedDescription)") + } + } + return nil + } + + public func setDict(_ dict: Dictionary, key: String) throws { + let encoder = JSONEncoder() + let jsonData = try encoder.encode(dict) + try APKeychain.keychain.set(jsonData, key: key) + } + + public func remove(_ key: String) throws -> Void { + try APKeychain.keychain.remove(key) + } +} + +public struct APDefaults { + public var string: (String) -> String? = { UserDefaults.standard.string(forKey: $0) } + public func string(forKey key: String) -> String? { + string(key) + } + + public var date: (String) -> Date? = { Date(timeIntervalSince1970: UserDefaults.standard.double(forKey: $0)) } + public func date(forKey key: String) -> Date? { + date(key) + } + + public var setDate: (Date?, String) -> Void = { UserDefaults.standard.set($0?.timeIntervalSince1970, forKey: $1) } + public func setDate(_ value: Date?, forKey key: String) { + setDate(value, key) + } + + public var set: (Any?, String) -> Void = { UserDefaults.standard.set($0, forKey: $1) } + public func set(_ value: Any?, forKey key: String) { + set(value, key) + } + + public var removeObject: (String) -> Void = { UserDefaults.standard.removeObject(forKey: $0) } + public func removeObject(forKey key: String) { + removeObject(key) + } + + public var get: (String) -> Any? = { UserDefaults.standard.value(forKey: $0) } + public func get(forKey key: String) -> Any? { + get(key) + } + + public var bool: (String) -> Bool? = { UserDefaults.standard.bool(forKey: $0) } + public func bool(forKey key: String) -> Bool? { + bool(key) + } +} diff --git a/AppleParty/AppleParty/Shared/Utils/ARLogs.swift b/AppleParty/AppleParty/Shared/Utils/ARLogs.swift new file mode 100644 index 0000000..f2ef46b --- /dev/null +++ b/AppleParty/AppleParty/Shared/Utils/ARLogs.swift @@ -0,0 +1,61 @@ +// +// APLogs.swift +// AppleParty +// +// Created by iHTC on 20210930. +// Copyright © 2021 37 Mobile Games. All rights reserved. +// + +import Foundation + + +class APLogs { + static let shared = APLogs() + + func add(_ log: String, printlog: Bool = false, retry: Int = 3) { + let dateFormatter : DateFormatter = DateFormatter() + dateFormatter.dateFormat = "[MM-dd HH:mm:ss] " + let currentDateString = dateFormatter.string(from: Date()) + let out = currentDateString + log + + if printlog { + debugPrint(out) + } + + dateFormatter.dateFormat = "yyyyMMdd_HH00" + let dataPath = dateFormatter.string(from: Date()) + + var documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + documentsURL.appendPathComponent("AppleParty") + documentsURL.appendPathComponent("Logs") + documentsURL.appendPathComponent("\(dataPath)_log.txt") + createFileDirectory(url: documentsURL) + + let path = documentsURL + do { + try out.appendLine(to: path) + } catch { + if retry > 0 { + print("‼️ retry save logs file~ error:\(error)") + add(log, printlog: printlog, retry: retry - 1) + } + } + } + + + func createFileDirectory(url: URL) { + let fm = FileManager.default + let directory = url.deletingLastPathComponent() + var isDirectory: ObjCBool = false + // 保证目录存在,不存在就创建目录 + if !(fm.fileExists(atPath: directory.path, isDirectory: &isDirectory) && isDirectory.boolValue) { + do { + try fm.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil) + } catch { + print("‼️ create Directory file error:\(error), \(url.path)") + } + } + } + + +} diff --git a/AppleParty/AppleParty/Shared/Utils/EmailUtils.swift b/AppleParty/AppleParty/Shared/Utils/EmailUtils.swift new file mode 100644 index 0000000..f774be1 --- /dev/null +++ b/AppleParty/AppleParty/Shared/Utils/EmailUtils.swift @@ -0,0 +1,79 @@ +// +// EmailUtils.swift +// AppleParty +// +// Created by HTC on 2021/9/09. +// Copyright © 2021 37 Mobile Games. All rights reserved. +// + +import AppKit +import Foundation +import SwiftSMTP + + +class EmailUtils: NSObject { + static func performSend(subject: String, recipients: [String], items: [Any]) { + let service = NSSharingService(named: NSSharingService.Name.composeEmail)! + service.recipients = recipients + service.subject = subject + service.perform(withItems: items) + } + + static func autoSend(subject: String, recipients: [String], htmlContent: String, _ textContent: String = "", attachmentPath: String = "", config: EamilConfigs, retry: Int = 3, completion: ((Error?) -> Void)? = nil) { + autoSendAtts(subject: subject, recipients: recipients, htmlContent: htmlContent, textContent, attachmentFiles: [attachmentPath], config: config, retry: retry, completion: completion) + } + + static func autoSendAtts(subject: String, recipients: [String], htmlContent: String, _ textContent: String = "", attachmentFiles: Array = [], config: EamilConfigs, retry: Int = 3, completion: ((Error?) -> Void)? = nil) { + + let smtp = SMTP( + hostname: config.smtp, // SMTP server address + email: config.addr, // username to login + password: config.pwd // password to login + ) + + let drLight = Mail.User(name: config.name, email: config.addr) + var megamans = [Mail.User]() + for email in recipients { + let megaman = Mail.User(name: "", email: email) + megamans.append(megaman) + } + // Create an HTML `Attachment` + let htmlAttachment = Attachment( + htmlContent: htmlContent + // To reference `fileAttachment` + ) + + var attachments: [Attachment] = [] + attachments.append(htmlAttachment) + + // Create a file `Attachment` + attachmentFiles.forEach { filePath in + if !filePath.isEmpty { + let fileAttachment = Attachment(filePath: filePath) + attachments.append(fileAttachment) + } + } + + let mail = Mail( + from: drLight, + to: megamans, + subject: subject, + text: textContent, + attachments: attachments + ) + + smtp.send(mail) { (error) in + if let error = error { + print("SendEmail error: \(error)") + if retry > 0 { + autoSendAtts(subject: subject, recipients: recipients, htmlContent: htmlContent, textContent, attachmentFiles: attachmentFiles, config: config, retry: retry - 1, completion: completion) + return + } + } + debugPrint("email success~") + if let block = completion { + block(error) + } + } + } +} diff --git a/AppleParty/AppleParty/Shared/Utils/FoundationUtil.swift b/AppleParty/AppleParty/Shared/Utils/FoundationUtil.swift new file mode 100644 index 0000000..ecafbf5 --- /dev/null +++ b/AppleParty/AppleParty/Shared/Utils/FoundationUtil.swift @@ -0,0 +1,303 @@ +// +// FoundationUtil.swift +// AppleParty +// +// Created by HTC on 2022/3/17. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import AppKit +import Cocoa +import CommonCrypto + + +// MARK: - Extension +// refer: https://stackoverflow.com/questions/27327067/append-text-or-data-to-text-file-in-swift +extension String { + + func appendLine(to url: URL) throws { + try self.appending("\n").append(to: url) + } + func append(to url: URL) throws { + let data = self.data(using: String.Encoding.utf8) + try data?.append(to: url) + } + + func trim() -> String { + var resultString = self.trimmingCharacters(in: CharacterSet.whitespaces) + resultString = resultString.trimmingCharacters(in: CharacterSet.newlines) + return resultString + } + + func matchRegex(regex: String, options: NSRegularExpression.Options = []) -> Bool { + guard let re = try? NSRegularExpression(pattern: regex, options: options) else { + return false + } + return re.firstMatch(in: self, options: [], range: NSMakeRange(0, utf16.count)) != nil + } + + func md5() -> String { + let cStr = self.cString(using: String.Encoding.utf8) + let buffer = UnsafeMutablePointer.allocate(capacity: 16) + CC_MD5(cStr!, (CC_LONG)(strlen(cStr!)), buffer) + let md5String = NSMutableString() + for i in 0 ..< 16 { + md5String.appendFormat("%02x", buffer[i]) + } + free(buffer) + return md5String as String + } + + func prettyJSON(_ options: JSONSerialization.WritingOptions = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes, .fragmentsAllowed] ) throws -> String? { + let data = self.data(using: .utf8)! + let obj = try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) + let jsonData = try JSONSerialization.data(withJSONObject: obj, options: options) + let pretty = String(data: jsonData, encoding: .utf8) + return pretty + } + + // MARK: - 价格格式调整 + + /// 返回保留2位小数的价格格式 + /// 因为苹果接口返回的价格可能是 "3","3.0" 或 "3.00" + /// - Parameter price: 原价格 + /// - Returns: 保留2位小数的价格字符串 + func normalizePrice() -> String { + let price = self + let components = price.split(separator: ".") + if components.count == 1 { + return price + ".00" + } else if components.count == 2 { + let decimalPart = components[1] + if decimalPart.count == 1 { + return "\(components[0]).\(decimalPart)0" + } else if decimalPart.count >= 2 { + return "\(components[0]).\(decimalPart.prefix(2))" + } + } + return price + } + + /// 返回最多保留2位小数的价格格式 + /// 例如: "74.989999999999995 转换为 "74.99" + /// - Returns: 超过2个小数位的会四舍五入,最终的价格格式可能示例: "3","3.0" 或 "3.00" + func twoDecimalPrice() -> String { + let input = self + let parts = input.split(separator: ".") + if parts.count == 2, parts[1].count > 2, let value = Double(input) { + let roundedValue = round(value * 100) / 100 + return String(format: "%.2f", roundedValue) + } + + return input + } +} + +extension Data { + func append(to url: URL) throws { + if let fileHandle = try? FileHandle(forWritingTo: url) { + defer { + fileHandle.closeFile() + } + fileHandle.seekToEndOfFile() + fileHandle.write(self) + } else { + try write(to: url) + } + } +} + +// MARK: - Unicode string +// refer: https://github.com/Geniune/SwiftUnicode/blob/master/ObjectUnicode.swift +extension Array { + var unicodeDescription : String { + return self.description.stringByReplaceUnicode + } +} + +extension Dictionary { + var unicodeDescription : String{ + return self.description.stringByReplaceUnicode + } +} + +extension String { + var unicodeDescription : String { + return self.stringByReplaceUnicode + } + + var stringByReplaceUnicode : String { + let tempStr1 = self.replacingOccurrences(of: "\\u", with: "\\U") + let tempStr2 = tempStr1.replacingOccurrences(of: "\"", with: "\\\"") + let tempStr3 = "\"".appending(tempStr2).appending("\"") + let tempData = tempStr3.data(using: String.Encoding.utf8) + var returnStr:String = "" + do { + returnStr = try PropertyListSerialization.propertyList(from: tempData!, options: [.mutableContainers], format: nil) as! String + } catch { + print(error) + } + return returnStr.replacingOccurrences(of: "\\n", with: "\n") + } + + var createFilePath: URL { + var documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + documentsURL.appendPathComponent(self) + // createFileDirectory + let fm = FileManager.default + let directory = documentsURL.deletingLastPathComponent() + var isDirectory: ObjCBool = false + // 保证目录存在,不存在就创建目录 + if !(fm.fileExists(atPath: directory.path, isDirectory: &isDirectory) && isDirectory.boolValue) { + do { + try fm.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil) + } catch { + print("‼️ create Directory file error:\(error)") + } + } + return documentsURL + + } +} + +extension URL { + + func fileMD5() -> String? { + let bufferSize = 1024 * 1024 + do { + //打开文件 + let file = try FileHandle(forReadingFrom: self) + defer { + file.closeFile() + } + + //初始化内容 + var context = CC_MD5_CTX() + CC_MD5_Init(&context) + + //读取文件信息 + while case let data = file.readData(ofLength: bufferSize), data.count > 0 { + data.withUnsafeBytes { + _ = CC_MD5_Update(&context, $0, CC_LONG(data.count)) + } + } + + //计算Md5摘要 + var digest = Data(count: Int(CC_MD5_DIGEST_LENGTH)) + digest.withUnsafeMutableBytes { + _ = CC_MD5_Final($0, &context) + } + + return digest.map { String(format: "%02hhx", $0) }.joined() + + } catch { + print("Cannot open file:", error.localizedDescription) + return nil + } + } + + func fileSize() -> String { + if let fileData:Data = try? Data.init(contentsOf: self) { + return String(fileData.count) + } + return "0" + } + + func fileSizeInt() -> Int { + if let fileData: Data = try? Data.init(contentsOf: self) { + return fileData.count + } + return 0 + } + +} + +extension Collection { + var isNotEmpty: Bool { return !isEmpty } +} + +/// 进行类型转换获取字符串类型值 +func string(from value: Any?, defaultValue: String = "") -> String { + if let str = value as? String { + return str + } else if let int = value as? Int { + return int.description + } else if let double = value as? Double { + return double.description + } else if let float = value as? Float { + return float.description + } else { + return defaultValue + } +} + + +/// 进行类型转换获取Int类型值 +func int(from value: Any?) -> Int? { + if let num = value as? Int { + return num + } else if let num = value as? String { + return Int(num) + } else { + return nil + } +} + +/// 判断字符串类型转化为bool值,1代表true,其他代表false +func bool(from value: Any?) -> Bool { + if let str = value as? String { + if str == "1" { + return true + } + } + if let num = value as? Int { + if num == 1 { + return true + } + } + + if let boo = value as? Bool { + return boo + } + return false +} + +/// 进行类型转换获取[String: Any]类型值 +func dictionary(_ origin: Any?) -> [String: Any] { + return origin as? [String: Any] ?? [:] +} + +func dictionaryArray(_ origin: Any?) -> [[String: Any]] { + return origin as? [[String: Any]] ?? [[String: Any]]() +} + +func stringArray(_ origin: Any?) -> [String] { + return origin as? [String] ?? [] +} + +/// 校验邮箱地址 +func isEmailValid(_ str: String) -> Bool { + guard str.matchRegex(regex: "^.+@.+\\..+$") else { + return false + } + return true +} + +func currentView() -> NSView { + if let rootView = NSApplication.shared.keyWindow?.contentViewController?.view { + return rootView + } + + if let delegate = NSApplication.shared.delegate as? AppDelegate { + return delegate.mainWindow?.contentView ?? NSView() + } + + debugPrint("Fatal error: window or keyWindow is nil") + return NSView() +} + +func debugLog(_ items: Any..., separator: String = " ", terminator: String = "\n") { + #if DEBUG + debugPrint(items, separator: separator, terminator: terminator) + #endif +} diff --git a/AppleParty/AppleParty/SparkleUpdate/AppleParty-release.html b/AppleParty/AppleParty/SparkleUpdate/AppleParty-release.html new file mode 100644 index 0000000..616a60f --- /dev/null +++ b/AppleParty/AppleParty/SparkleUpdate/AppleParty-release.html @@ -0,0 +1,278 @@ + + + + + + + AppleParty 更新说明 + + + +
+

v3.8.02025-09-29

+
    +
  • 修复苹果登录内购价格调整失败的问题
  • +
+
+
+

v3.7.02024-10-29

+
    +
  • 修复苹果登录接口升级为 Secure Remote Password (SRP),导致登录失败问题
  • +
  • 增加上传批量内购时,是否显示 ASC API 请求速率的阈值的开关
  • +
  • 修复切换账号接口导致财务报表下载失败问题
  • +
+
+
+

v3.3.02024-01-26

+
    +
  • 修复 App Store Connect API 3.2 字段弃用导致无法上传批量内购问题
  • +
+
+
+

v3.2.02023-12-29

+
    +
  • 新增内购上传时,显示接口速率限制和剩余的次数
  • +
  • 修复切换账号接口变更问题
  • +
+
+
+

v3.1.02023-04-28

+
    +
  • 新增内购凭证验证工具,用于校验 IAP 凭证
  • +
  • 批量内购创建:审核备注原来有值,而表格未填写值时,则使用 ASC 后台的原值
  • +
+
+
+

v3.0.02023-04-23

+
    +
  • 新增支持苹果新价格机制,基准国家和定价价格等配置
  • +
  • 内购支持销售范围配置,包括国家或地区,将来新国家/地区自动提供销售等
  • +
  • 注意:需要使用新的内购价格表格!
  • +
  • 打开 App 默认居中显示
  • +
+
+
+

v2.1.12023-02-01

+
    +
  • 修复 MBProgressHUD-OSX 仓库删除导致无法构建问题
  • +
+
+
+

v2.1.02022-12-25

+
    +
  • 通过 AppStoreConnect API 批量创建内购商品功能
  • +
  • 增加上传 IPA 文件功能
  • +
+
+
+

v2.0.52022-07-13

+
    +
  • 修复苹果 App 列表接口变更导致 App 功能无法使用的问题
  • +
+
+
+

v2.0.42022-04-10

+
    +
  • 修复勾选信任设备后不生效的问题
  • +
+
+
+

v2.0.32022-04-09

+
    +
  • 增加是否记住密码、信任设备的勾选项
  • +
  • 切换账号从双点确认改为单击确认切换
  • +
  • 修复和完善一些Bug和体验的问题,欢迎大家反馈~
  • +
+
+
+

v2.0.22022-04-07

+
    +
  • 修复账号切换登陆后没有记住登陆状态的问题
  • +
  • 优化账号登陆选择弹窗,从双击选择改为单击选择
  • +
  • 优化完善一些体验细节,欢迎大家反馈~
  • +
+
+
+

v2.0.12022-04-06

+
    +
  • 修复导出内购项目表csv文件中文乱码和分隔可能错误的问题
  • +
  • 修复更新弹窗内容显示为文本的问题
  • +
+
+
+

What's AppleParty

+
+ AppleParty 是三七互娱旗下37手游iOS团队研发,实现快速操作 App Store Connect 后台的自动化 macOS 工具。 +
    +
  • 内购买项目管理;
  • +
  • 批量内购买项目创建和更新;
  • +
  • 批量商店图和预览视频上传和更新;
  • +
  • 邮件发送工具;
  • +
  • 二维码扫描和生成工具;
  • +
  • 数据报表下载和同步(未来开源);
  • +
  • 更多功能,敬请期待~
  • +
+
+
+ + + + diff --git a/AppleParty/AppleParty/SparkleUpdate/update.xml b/AppleParty/AppleParty/SparkleUpdate/update.xml new file mode 100644 index 0000000..d64ad6e --- /dev/null +++ b/AppleParty/AppleParty/SparkleUpdate/update.xml @@ -0,0 +1,22 @@ + + + + AppleParty for macOS Changelog + Most recent changes with links to updates. + zh_CN + + Version 3.8.0 + https://37mobileteam.github.io/Sparkle/AppleParty-release.html + 2025-09-29 + + 11.0 + + + diff --git a/AppleParty/AppleParty/Vendors/ITMS/GDataXMLNode.h b/AppleParty/AppleParty/Vendors/ITMS/GDataXMLNode.h new file mode 100644 index 0000000..97dae10 --- /dev/null +++ b/AppleParty/AppleParty/Vendors/ITMS/GDataXMLNode.h @@ -0,0 +1,225 @@ +/* Copyright (c) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// These node, element, and document classes implement a subset of the methods +// provided by NSXML. While NSXML behavior is mimicked as much as possible, +// there are important differences. +// +// The biggest difference is that, since this is based on libxml2, there +// is no retain model for the underlying node data. Rather than copy every +// node obtained from a parse tree (which would have a substantial memory +// impact), we rely on weak references, and it is up to the code that +// created a document to retain it for as long as any +// references rely on nodes inside that document tree. + + +#import + +// libxml includes require that the target Header Search Paths contain +// +// /usr/include/libxml2 +// +// and Other Linker Flags contain +// +// -lxml2 + +#ifndef LIBXML_VERSION +// Forward declaration of types when not included. + +struct _xmlNode; +typedef struct _xmlNode xmlNode; +typedef xmlNode* xmlNodePtr; +struct _xmlDoc; +typedef struct _xmlDoc xmlDoc; +typedef xmlDoc *xmlDocPtr; +#endif + +#ifdef GDATA_TARGET_NAMESPACE + // we're using target namespace macros + #import "GDataDefines.h" +#endif + +#undef _EXTERN +#undef _INITIALIZE_AS +#ifdef GDATAXMLNODE_DEFINE_GLOBALS +#define _EXTERN +#define _INITIALIZE_AS(x) =x +#else +#if defined(__cplusplus) +#define _EXTERN extern "C" +#else +#define _EXTERN extern +#endif +#define _INITIALIZE_AS(x) +#endif + +// when no namespace dictionary is supplied for XPath, the default namespace +// for the evaluated tree is registered with the prefix _def_ns +_EXTERN const char* kGDataXMLXPathDefaultNamespacePrefix _INITIALIZE_AS("_def_ns"); + +// Nomenclature for method names: +// +// Node = GData node +// XMLNode = xmlNodePtr +// +// So, for example: +// + (id)nodeConsumingXMLNode:(xmlNodePtr)theXMLNode; + +@class NSArray, NSDictionary, NSError, NSString, NSURL; +@class GDataXMLElement, GDataXMLDocument; + +enum { + GDataXMLInvalidKind = 0, + GDataXMLDocumentKind, + GDataXMLElementKind, + GDataXMLAttributeKind, + GDataXMLNamespaceKind, + GDataXMLProcessingInstructionKind, + GDataXMLCommentKind, + GDataXMLTextKind, + GDataXMLDTDKind, + GDataXMLEntityDeclarationKind, + GDataXMLAttributeDeclarationKind, + GDataXMLElementDeclarationKind, + GDataXMLNotationDeclarationKind +}; + +typedef NSUInteger GDataXMLNodeKind; + +@interface GDataXMLNode : NSObject { +@protected + // NSXMLNodes can have a namespace URI or prefix even if not part + // of a tree; xmlNodes cannot. When we create nodes apart from + // a tree, we'll store the dangling prefix or URI in the xmlNode's name, + // like + // "prefix:name" + // or + // "{http://uri}:name" + // + // We will fix up the node's namespace and name (and those of any children) + // later when adding the node to a tree with addChild: or addAttribute:. + // See fixUpNamespacesForNode:. + + xmlNodePtr xmlNode_; // may also be an xmlAttrPtr or xmlNsPtr + BOOL shouldFreeXMLNode_; // if yes, xmlNode_ will be free'd in dealloc + + // cached values + NSString *cachedName_; + NSArray *cachedChildren_; + NSArray *cachedAttributes_; +} + ++ (GDataXMLElement *)elementWithName:(NSString *)name; ++ (GDataXMLElement *)elementWithName:(NSString *)name stringValue:(NSString *)value; ++ (GDataXMLElement *)elementWithName:(NSString *)name URI:(NSString *)value; + ++ (id)attributeWithName:(NSString *)name stringValue:(NSString *)value; ++ (id)attributeWithName:(NSString *)name URI:(NSString *)attributeURI stringValue:(NSString *)value; + ++ (id)namespaceWithName:(NSString *)name stringValue:(NSString *)value; + ++ (id)textWithStringValue:(NSString *)value; + +- (NSString *)stringValue; +- (void)setStringValue:(NSString *)str; + +- (NSUInteger)childCount; +- (NSArray *)children; +- (GDataXMLNode *)childAtIndex:(unsigned)index; + +- (NSString *)localName; +- (NSString *)name; +- (NSString *)prefix; +- (NSString *)URI; + +- (GDataXMLNodeKind)kind; + +- (NSString *)XMLString; + ++ (NSString *)localNameForName:(NSString *)name; ++ (NSString *)prefixForName:(NSString *)name; + +// This is the preferred entry point for nodesForXPath. This takes an explicit +// namespace dictionary (keys are prefixes, values are URIs). +- (NSArray *)nodesForXPath:(NSString *)xpath namespaces:(NSDictionary *)namespaces error:(NSError **)error; + +// This implementation of nodesForXPath registers namespaces only from the +// document's root node. _def_ns may be used as a prefix for the default +// namespace, though there's no guarantee that the default namespace will +// be consistenly the same namespace in server responses. +- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error; + +// access to the underlying libxml node; be sure to release the cached values +// if you change the underlying tree at all +- (xmlNodePtr)XMLNode; +- (void)releaseCachedValues; + +@end + + +@interface GDataXMLElement : GDataXMLNode + +- (id)initWithXMLString:(NSString *)str error:(NSError **)error; + +- (NSArray *)namespaces; +- (void)setNamespaces:(NSArray *)namespaces; +- (void)addNamespace:(GDataXMLNode *)aNamespace; + +// addChild adds a copy of the child node to the element +- (void)addChild:(GDataXMLNode *)child; +- (void)removeChild:(GDataXMLNode *)child; + +- (NSArray *)elementsForName:(NSString *)name; +- (NSArray *)elementsForLocalName:(NSString *)localName URI:(NSString *)URI; + +- (NSArray *)attributes; +- (GDataXMLNode *)attributeForName:(NSString *)name; +- (GDataXMLNode *)attributeForLocalName:(NSString *)name URI:(NSString *)attributeURI; +- (void)addAttribute:(GDataXMLNode *)attribute; + +- (NSString *)resolvePrefixForNamespaceURI:(NSString *)namespaceURI; + +@end + +@interface GDataXMLDocument : NSObject { +@protected + xmlDoc* xmlDoc_; // strong; always free'd in dealloc +} + +- (id)initWithXMLString:(NSString *)str options:(unsigned int)mask error:(NSError **)error; +- (id)initWithData:(NSData *)data options:(unsigned int)mask error:(NSError **)error; + +// initWithRootElement uses a copy of the argument as the new document's root +- (id)initWithRootElement:(GDataXMLElement *)element; + +- (GDataXMLElement *)rootElement; + +- (NSData *)XMLData; + +- (void)setVersion:(NSString *)version; +- (void)setCharacterEncoding:(NSString *)encoding; + +// This is the preferred entry point for nodesForXPath. This takes an explicit +// namespace dictionary (keys are prefixes, values are URIs). +- (NSArray *)nodesForXPath:(NSString *)xpath namespaces:(NSDictionary *)namespaces error:(NSError **)error; + +// This implementation of nodesForXPath registers namespaces only from the +// document's root node. _def_ns may be used as a prefix for the default +// namespace, though there's no guarantee that the default namespace will +// be consistenly the same namespace in server responses. +- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error; + +- (NSString *)description; +@end diff --git a/AppleParty/AppleParty/Vendors/ITMS/GDataXMLNode.m b/AppleParty/AppleParty/Vendors/ITMS/GDataXMLNode.m new file mode 100644 index 0000000..e102a5f --- /dev/null +++ b/AppleParty/AppleParty/Vendors/ITMS/GDataXMLNode.m @@ -0,0 +1,1837 @@ +/* Copyright (c) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import +#import +#import + +#define GDATAXMLNODE_DEFINE_GLOBALS 1 +#import "GDataXMLNode.h" + +@class NSArray, NSDictionary, NSError, NSString, NSURL; +@class GDataXMLElement, GDataXMLDocument; + + +static const int kGDataXMLParseOptions = (XML_PARSE_NOCDATA | XML_PARSE_NOBLANKS); + +// dictionary key callbacks for string cache +static const void *StringCacheKeyRetainCallBack(CFAllocatorRef allocator, const void *str); +static void StringCacheKeyReleaseCallBack(CFAllocatorRef allocator, const void *str); +static CFStringRef StringCacheKeyCopyDescriptionCallBack(const void *str); +static Boolean StringCacheKeyEqualCallBack(const void *str1, const void *str2); +static CFHashCode StringCacheKeyHashCallBack(const void *str); + +// isEqual: has the fatal flaw that it doesn't deal well with the received +// being nil. We'll use this utility instead. + +// Static copy of AreEqualOrBothNil from GDataObject.m, so that using +// GDataXMLNode does not require pulling in all of GData. +static BOOL AreEqualOrBothNilPrivate(id obj1, id obj2) { + if (obj1 == obj2) { + return YES; + } + if (obj1 && obj2) { + return [obj1 isEqual:obj2]; + } + return NO; +} + + +// convert NSString* to xmlChar* +// +// the "Get" part implies that ownership remains with str + +static xmlChar* GDataGetXMLString(NSString *str) { + xmlChar* result = (xmlChar *)[str UTF8String]; + return result; +} + +// Make a fake qualified name we use as local name internally in libxml +// data structures when there's no actual namespace node available to point to +// from an element or attribute node +// +// Returns an autoreleased NSString* + +static NSString *GDataFakeQNameForURIAndName(NSString *theURI, NSString *name) { + + NSString *localName = [GDataXMLNode localNameForName:name]; + NSString *fakeQName = [NSString stringWithFormat:@"{%@}:%@", + theURI, localName]; + return fakeQName; +} + + +// libxml2 offers xmlSplitQName2, but that searches forwards. Since we may +// be searching for a whole URI shoved in as a prefix, like +// {http://foo}:name +// we'll search for the prefix in backwards from the end of the qualified name +// +// returns a copy of qname as the local name if there's no prefix +static xmlChar *SplitQNameReverse(const xmlChar *qname, xmlChar **prefix) { + + // search backwards for a colon + int qnameLen = xmlStrlen(qname); + for (int idx = qnameLen - 1; idx >= 0; idx--) { + + if (qname[idx] == ':') { + + // found the prefix; copy the prefix, if requested + if (prefix != NULL) { + if (idx > 0) { + *prefix = xmlStrsub(qname, 0, idx); + } else { + *prefix = NULL; + } + } + + if (idx < qnameLen - 1) { + // return a copy of the local name + xmlChar *localName = xmlStrsub(qname, idx + 1, qnameLen - idx - 1); + return localName; + } else { + return NULL; + } + } + } + + // no colon found, so the qualified name is the local name + xmlChar *qnameCopy = xmlStrdup(qname); + return qnameCopy; +} + +@interface GDataXMLNode (PrivateMethods) + +// consuming a node implies it will later be freed when the instance is +// dealloc'd; borrowing it implies that ownership and disposal remain the +// job of the supplier of the node + ++ (id)nodeConsumingXMLNode:(xmlNodePtr)theXMLNode; +- (id)initConsumingXMLNode:(xmlNodePtr)theXMLNode; + ++ (id)nodeBorrowingXMLNode:(xmlNodePtr)theXMLNode; +- (id)initBorrowingXMLNode:(xmlNodePtr)theXMLNode; + +// getters of the underlying node +- (xmlNodePtr)XMLNode; +- (xmlNodePtr)XMLNodeCopy; + +// search for an underlying attribute +- (GDataXMLNode *)attributeForXMLNode:(xmlAttrPtr)theXMLNode; + +// return an NSString for an xmlChar*, using our strings cache in the +// document +- (NSString *)stringFromXMLString:(const xmlChar *)chars; + +// setter/getter of the dealloc flag for the underlying node +- (BOOL)shouldFreeXMLNode; +- (void)setShouldFreeXMLNode:(BOOL)flag; + +@end + +@interface GDataXMLElement (PrivateMethods) + ++ (void)fixUpNamespacesForNode:(xmlNodePtr)nodeToFix + graftingToTreeNode:(xmlNodePtr)graftPointNode; +@end + +@implementation GDataXMLNode + ++ (void)load { + xmlInitParser(); +} + +// Note on convenience methods for making stand-alone element and +// attribute nodes: +// +// Since we're making a node from scratch, we don't +// have any namespace info. So the namespace prefix, if +// any, will just be slammed into the node name. +// We'll rely on the -addChild method below to remove +// the namespace prefix and replace it with a proper ns +// pointer. + ++ (GDataXMLElement *)elementWithName:(NSString *)name { + + xmlNodePtr theNewNode = xmlNewNode(NULL, // namespace + GDataGetXMLString(name)); + if (theNewNode) { + // succeeded + return [self nodeConsumingXMLNode:theNewNode]; + } + return nil; +} + ++ (GDataXMLElement *)elementWithName:(NSString *)name stringValue:(NSString *)value { + + xmlNodePtr theNewNode = xmlNewNode(NULL, // namespace + GDataGetXMLString(name)); + if (theNewNode) { + + xmlNodePtr textNode = xmlNewText(GDataGetXMLString(value)); + if (textNode) { + + xmlNodePtr temp = xmlAddChild(theNewNode, textNode); + if (temp) { + // succeeded + return [self nodeConsumingXMLNode:theNewNode]; + } + } + + // failed; free the node and any children + xmlFreeNode(theNewNode); + } + return nil; +} + ++ (GDataXMLElement *)elementWithName:(NSString *)name URI:(NSString *)theURI { + + // since we don't know a prefix yet, shove in the whole URI; we'll look for + // a proper namespace ptr later when addChild calls fixUpNamespacesForNode + + NSString *fakeQName = GDataFakeQNameForURIAndName(theURI, name); + + xmlNodePtr theNewNode = xmlNewNode(NULL, // namespace + GDataGetXMLString(fakeQName)); + if (theNewNode) { + return [self nodeConsumingXMLNode:theNewNode]; + } + return nil; +} + ++ (id)attributeWithName:(NSString *)name stringValue:(NSString *)value { + + xmlChar *xmlName = GDataGetXMLString(name); + xmlChar *xmlValue = GDataGetXMLString(value); + + xmlAttrPtr theNewAttr = xmlNewProp(NULL, // parent node for the attr + xmlName, xmlValue); + if (theNewAttr) { + return [self nodeConsumingXMLNode:(xmlNodePtr) theNewAttr]; + } + + return nil; +} + ++ (id)attributeWithName:(NSString *)name URI:(NSString *)attributeURI stringValue:(NSString *)value { + + // since we don't know a prefix yet, shove in the whole URI; we'll look for + // a proper namespace ptr later when addChild calls fixUpNamespacesForNode + + NSString *fakeQName = GDataFakeQNameForURIAndName(attributeURI, name); + + xmlChar *xmlName = GDataGetXMLString(fakeQName); + xmlChar *xmlValue = GDataGetXMLString(value); + + xmlAttrPtr theNewAttr = xmlNewProp(NULL, // parent node for the attr + xmlName, xmlValue); + if (theNewAttr) { + return [self nodeConsumingXMLNode:(xmlNodePtr) theNewAttr]; + } + + return nil; +} + ++ (id)textWithStringValue:(NSString *)value { + + xmlNodePtr theNewText = xmlNewText(GDataGetXMLString(value)); + if (theNewText) { + return [self nodeConsumingXMLNode:theNewText]; + } + return nil; +} + ++ (id)namespaceWithName:(NSString *)name stringValue:(NSString *)value { + + xmlChar *href = GDataGetXMLString(value); + xmlChar *prefix; + + if ([name length] > 0) { + prefix = GDataGetXMLString(name); + } else { + // default namespace is represented by a nil prefix + prefix = nil; + } + + xmlNsPtr theNewNs = xmlNewNs(NULL, // parent node + href, prefix); + if (theNewNs) { + return [self nodeConsumingXMLNode:(xmlNodePtr) theNewNs]; + } + return nil; +} + ++ (id)nodeConsumingXMLNode:(xmlNodePtr)theXMLNode { + Class theClass; + + if (theXMLNode->type == XML_ELEMENT_NODE) { + theClass = [GDataXMLElement class]; + } else { + theClass = [GDataXMLNode class]; + } + return [[[theClass alloc] initConsumingXMLNode:theXMLNode] autorelease]; +} + +- (id)initConsumingXMLNode:(xmlNodePtr)theXMLNode { + self = [super init]; + if (self) { + xmlNode_ = theXMLNode; + shouldFreeXMLNode_ = YES; + } + return self; +} + ++ (id)nodeBorrowingXMLNode:(xmlNodePtr)theXMLNode { + Class theClass; + if (theXMLNode->type == XML_ELEMENT_NODE) { + theClass = [GDataXMLElement class]; + } else { + theClass = [GDataXMLNode class]; + } + + return [[[theClass alloc] initBorrowingXMLNode:theXMLNode] autorelease]; +} + +- (id)initBorrowingXMLNode:(xmlNodePtr)theXMLNode { + self = [super init]; + if (self) { + xmlNode_ = theXMLNode; + shouldFreeXMLNode_ = NO; + } + return self; +} + +- (void)releaseCachedValues { + + [cachedName_ release]; + cachedName_ = nil; + + [cachedChildren_ release]; + cachedChildren_ = nil; + + [cachedAttributes_ release]; + cachedAttributes_ = nil; +} + + +// convert xmlChar* to NSString* +// +// returns an autoreleased NSString*, from the current node's document strings +// cache if possible +- (NSString *)stringFromXMLString:(const xmlChar *)chars { + +#if DEBUG + NSCAssert(chars != NULL, @"GDataXMLNode sees an unexpected empty string"); +#endif + if (chars == NULL) return nil; + + CFMutableDictionaryRef cacheDict = NULL; + + NSString *result = nil; + + if (xmlNode_ != NULL + && (xmlNode_->type == XML_ELEMENT_NODE + || xmlNode_->type == XML_ATTRIBUTE_NODE + || xmlNode_->type == XML_TEXT_NODE)) { + // there is no xmlDocPtr in XML_NAMESPACE_DECL nodes, + // so we can't cache the text of those + + // look for a strings cache in the document + // + // the cache is in the document's user-defined _private field + + if (xmlNode_->doc != NULL) { + + cacheDict = xmlNode_->doc->_private; + + if (cacheDict) { + + // this document has a strings cache + result = (NSString *) CFDictionaryGetValue(cacheDict, chars); + if (result) { + // we found the xmlChar string in the cache; return the previously + // allocated NSString, rather than allocate a new one + return result; + } + } + } + } + + // allocate a new NSString for this xmlChar* + result = [NSString stringWithUTF8String:(const char *) chars]; + if (cacheDict) { + // save the string in the document's string cache + CFDictionarySetValue(cacheDict, chars, result); + } + + return result; +} + +- (void)dealloc { + + if (xmlNode_ && shouldFreeXMLNode_) { + xmlFreeNode(xmlNode_); + xmlNode_ = NULL; + } + + [self releaseCachedValues]; + [super dealloc]; +} + +#pragma mark - + +- (void)setStringValue:(NSString *)str { + if (xmlNode_ != NULL && str != nil) { + + if (xmlNode_->type == XML_NAMESPACE_DECL) { + + // for a namespace node, the value is the namespace URI + xmlNsPtr nsNode = (xmlNsPtr)xmlNode_; + + if (nsNode->href != NULL) xmlFree((char *)nsNode->href); + + nsNode->href = xmlStrdup(GDataGetXMLString(str)); + + } else { + + // attribute or element node + + // do we need to call xmlEncodeSpecialChars? + xmlNodeSetContent(xmlNode_, GDataGetXMLString(str)); + } + } +} + +- (NSString *)stringValue { + + NSString *str = nil; + + if (xmlNode_ != NULL) { + + if (xmlNode_->type == XML_NAMESPACE_DECL) { + + // for a namespace node, the value is the namespace URI + xmlNsPtr nsNode = (xmlNsPtr)xmlNode_; + + str = [self stringFromXMLString:(nsNode->href)]; + + } else { + + // attribute or element node + xmlChar* chars = xmlNodeGetContent(xmlNode_); + if (chars) { + + str = [self stringFromXMLString:chars]; + + xmlFree(chars); + } + } + } + return str; +} + +- (NSString *)XMLString { + + NSString *str = nil; + + if (xmlNode_ != NULL) { + + xmlBufferPtr buff = xmlBufferCreate(); + if (buff) { + + xmlDocPtr doc = NULL; + int level = 0; + int format = 0; + + int result = xmlNodeDump(buff, doc, xmlNode_, level, format); + + if (result > -1) { + str = [[[NSString alloc] initWithBytes:(xmlBufferContent(buff)) + length:(NSUInteger)(xmlBufferLength(buff)) + encoding:NSUTF8StringEncoding] autorelease]; + } + xmlBufferFree(buff); + } + } + + // remove leading and trailing whitespace + NSCharacterSet *ws = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + NSString *trimmed = [str stringByTrimmingCharactersInSet:ws]; + return trimmed; +} + +- (NSString *)localName { + NSString *str = nil; + + if (xmlNode_ != NULL) { + + str = [self stringFromXMLString:(xmlNode_->name)]; + + // if this is part of a detached subtree, str may have a prefix in it + str = [[self class] localNameForName:str]; + } + return str; +} + +- (NSString *)prefix { + + NSString *str = nil; + + if (xmlNode_ != NULL) { + + // the default namespace's prefix is an empty string, though libxml + // represents it as NULL for ns->prefix + str = @""; + + if (xmlNode_->ns != NULL && xmlNode_->ns->prefix != NULL) { + str = [self stringFromXMLString:(xmlNode_->ns->prefix)]; + } + } + return str; +} + +- (NSString *)URI { + + NSString *str = nil; + + if (xmlNode_ != NULL) { + + if (xmlNode_->ns != NULL && xmlNode_->ns->href != NULL) { + str = [self stringFromXMLString:(xmlNode_->ns->href)]; + } + } + return str; +} + +- (NSString *)qualifiedName { + // internal utility + + NSString *str = nil; + + if (xmlNode_ != NULL) { + if (xmlNode_->type == XML_NAMESPACE_DECL) { + + // name of a namespace node + xmlNsPtr nsNode = (xmlNsPtr)xmlNode_; + + // null is the default namespace; one is the loneliest number + if (nsNode->prefix == NULL) { + str = @""; + } + else { + str = [self stringFromXMLString:(nsNode->prefix)]; + } + + } else if (xmlNode_->ns != NULL && xmlNode_->ns->prefix != NULL) { + + // name of a non-namespace node + + // has a prefix + char *qname; + if (asprintf(&qname, "%s:%s", (const char *)xmlNode_->ns->prefix, + xmlNode_->name) != -1) { + str = [self stringFromXMLString:(const xmlChar *)qname]; + free(qname); + } + } else { + // lacks a prefix + str = [self stringFromXMLString:(xmlNode_->name)]; + } + } + + return str; +} + +- (NSString *)name { + + if (cachedName_ != nil) { + return cachedName_; + } + + NSString *str = [self qualifiedName]; + + cachedName_ = [str retain]; + + return str; +} + ++ (NSString *)localNameForName:(NSString *)name { + if (name != nil) { + + NSRange range = [name rangeOfString:@":"]; + if (range.location != NSNotFound) { + + // found a colon + if (range.location + 1 < [name length]) { + NSString *localName = [name substringFromIndex:(range.location + 1)]; + return localName; + } + } + } + return name; +} + ++ (NSString *)prefixForName:(NSString *)name { + if (name != nil) { + + NSRange range = [name rangeOfString:@":"]; + if (range.location != NSNotFound) { + + NSString *prefix = [name substringToIndex:(range.location)]; + return prefix; + } + } + return nil; +} + +- (NSUInteger)childCount { + + if (cachedChildren_ != nil) { + return [cachedChildren_ count]; + } + + if (xmlNode_ != NULL) { + + unsigned int count = 0; + + xmlNodePtr currChild = xmlNode_->children; + + while (currChild != NULL) { + ++count; + currChild = currChild->next; + } + return count; + } + return 0; +} + +- (NSArray *)children { + + if (cachedChildren_ != nil) { + return cachedChildren_; + } + + NSMutableArray *array = nil; + + if (xmlNode_ != NULL) { + + xmlNodePtr currChild = xmlNode_->children; + + while (currChild != NULL) { + GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:currChild]; + + if (array == nil) { + array = [NSMutableArray arrayWithObject:node]; + } else { + [array addObject:node]; + } + + currChild = currChild->next; + } + + cachedChildren_ = [array retain]; + } + return array; +} + +- (GDataXMLNode *)childAtIndex:(unsigned)index { + + NSArray *children = [self children]; + + if ([children count] > index) { + + return [children objectAtIndex:index]; + } + return nil; +} + +- (GDataXMLNodeKind)kind { + if (xmlNode_ != NULL) { + xmlElementType nodeType = xmlNode_->type; + switch (nodeType) { + case XML_ELEMENT_NODE: return GDataXMLElementKind; + case XML_ATTRIBUTE_NODE: return GDataXMLAttributeKind; + case XML_TEXT_NODE: return GDataXMLTextKind; + case XML_CDATA_SECTION_NODE: return GDataXMLTextKind; + case XML_ENTITY_REF_NODE: return GDataXMLEntityDeclarationKind; + case XML_ENTITY_NODE: return GDataXMLEntityDeclarationKind; + case XML_PI_NODE: return GDataXMLProcessingInstructionKind; + case XML_COMMENT_NODE: return GDataXMLCommentKind; + case XML_DOCUMENT_NODE: return GDataXMLDocumentKind; + case XML_DOCUMENT_TYPE_NODE: return GDataXMLDocumentKind; + case XML_DOCUMENT_FRAG_NODE: return GDataXMLDocumentKind; + case XML_NOTATION_NODE: return GDataXMLNotationDeclarationKind; + case XML_HTML_DOCUMENT_NODE: return GDataXMLDocumentKind; + case XML_DTD_NODE: return GDataXMLDTDKind; + case XML_ELEMENT_DECL: return GDataXMLElementDeclarationKind; + case XML_ATTRIBUTE_DECL: return GDataXMLAttributeDeclarationKind; + case XML_ENTITY_DECL: return GDataXMLEntityDeclarationKind; + case XML_NAMESPACE_DECL: return GDataXMLNamespaceKind; + case XML_XINCLUDE_START: return GDataXMLProcessingInstructionKind; + case XML_XINCLUDE_END: return GDataXMLProcessingInstructionKind; + case XML_DOCB_DOCUMENT_NODE: return GDataXMLDocumentKind; + } + } + return GDataXMLInvalidKind; +} + +- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error { + // call through with no explicit namespace dictionary; that will register the + // root node's namespaces + return [self nodesForXPath:xpath namespaces:nil error:error]; +} + +- (NSArray *)nodesForXPath:(NSString *)xpath + namespaces:(NSDictionary *)namespaces + error:(NSError **)error { + + NSMutableArray *array = nil; + NSInteger errorCode = -1; + NSDictionary *errorInfo = nil; + + // xmlXPathNewContext requires a doc for its context, but if our elements + // are created from GDataXMLElement's initWithXMLString there may not be + // a document. (We may later decide that we want to stuff the doc used + // there into a GDataXMLDocument and retain it, but we don't do that now.) + // + // We'll temporarily make a document to use for the xpath context. + + xmlDocPtr tempDoc = NULL; + xmlNodePtr topParent = NULL; + + if (xmlNode_->doc == NULL) { + tempDoc = xmlNewDoc(NULL); + if (tempDoc) { + // find the topmost node of the current tree to make the root of + // our temporary document + topParent = xmlNode_; + while (topParent->parent != NULL) { + topParent = topParent->parent; + } + xmlDocSetRootElement(tempDoc, topParent); + } + } + + if (xmlNode_ != NULL && xmlNode_->doc != NULL) { + + xmlXPathContextPtr xpathCtx = xmlXPathNewContext(xmlNode_->doc); + if (xpathCtx) { + // anchor at our current node + xpathCtx->node = xmlNode_; + + // if a namespace dictionary was provided, register its contents + if (namespaces) { + // the dictionary keys are prefixes; the values are URIs + for (NSString *prefix in namespaces) { + NSString *uri = [namespaces objectForKey:prefix]; + + xmlChar *prefixChars = (xmlChar *) [prefix UTF8String]; + xmlChar *uriChars = (xmlChar *) [uri UTF8String]; + int result = xmlXPathRegisterNs(xpathCtx, prefixChars, uriChars); + if (result != 0) { +#if DEBUG + NSCAssert1(result == 0, @"GDataXMLNode XPath namespace %@ issue", + prefix); +#endif + } + } + } else { + // no namespace dictionary was provided + // + // register the namespaces of this node, if it's an element, or of + // this node's root element, if it's a document + xmlNodePtr nsNodePtr = xmlNode_; + if (xmlNode_->type == XML_DOCUMENT_NODE) { + nsNodePtr = xmlDocGetRootElement((xmlDocPtr) xmlNode_); + } + + // step through the namespaces, if any, and register each with the + // xpath context + if (nsNodePtr != NULL) { + for (xmlNsPtr nsPtr = nsNodePtr->ns; nsPtr != NULL; nsPtr = nsPtr->next) { + + // default namespace is nil in the tree, but there's no way to + // register a default namespace, so we'll register a fake one, + // _def_ns + const xmlChar* prefix = nsPtr->prefix; + if (prefix == NULL) { + prefix = (xmlChar*) kGDataXMLXPathDefaultNamespacePrefix; + } + + int result = xmlXPathRegisterNs(xpathCtx, prefix, nsPtr->href); + if (result != 0) { +#if DEBUG + NSCAssert1(result == 0, @"GDataXMLNode XPath namespace %s issue", + prefix); +#endif + } + } + } + } + + // now evaluate the path + xmlXPathObjectPtr xpathObj; + xpathObj = xmlXPathEval(GDataGetXMLString(xpath), xpathCtx); + if (xpathObj) { + + // we have some result from the search + array = [NSMutableArray array]; + + xmlNodeSetPtr nodeSet = xpathObj->nodesetval; + if (nodeSet) { + + // add each node in the result set to our array + for (int index = 0; index < nodeSet->nodeNr; index++) { + + xmlNodePtr currNode = nodeSet->nodeTab[index]; + + GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:currNode]; + if (node) { + [array addObject:node]; + } + } + } + xmlXPathFreeObject(xpathObj); + } else { + // provide an error for failed evaluation + const char *msg = xpathCtx->lastError.str1; + errorCode = xpathCtx->lastError.code; + if (msg) { + NSString *errStr = [NSString stringWithUTF8String:msg]; + errorInfo = [NSDictionary dictionaryWithObject:errStr + forKey:@"error"]; + } + } + + xmlXPathFreeContext(xpathCtx); + } + } else { + // not a valid node for using XPath + errorInfo = [NSDictionary dictionaryWithObject:@"invalid node" + forKey:@"error"]; + } + + if (array == nil && error != nil) { + *error = [NSError errorWithDomain:@"com.google.GDataXML" + code:errorCode + userInfo:errorInfo]; + } + + if (tempDoc != NULL) { + xmlUnlinkNode(topParent); + xmlSetTreeDoc(topParent, NULL); + xmlFreeDoc(tempDoc); + } + return array; +} + +- (NSString *)description { + int nodeType = (xmlNode_ ? (int)xmlNode_->type : -1); + + return [NSString stringWithFormat:@"%@ %p: {type:%d name:%@ xml:\"%@\"}", + [self class], self, nodeType, [self name], [self XMLString]]; +} + +- (id)copyWithZone:(NSZone *)zone { + + xmlNodePtr nodeCopy = [self XMLNodeCopy]; + + if (nodeCopy != NULL) { + return [[[self class] alloc] initConsumingXMLNode:nodeCopy]; + } + return nil; +} + +- (BOOL)isEqual:(GDataXMLNode *)other { + if (self == other) return YES; + if (![other isKindOfClass:[GDataXMLNode class]]) return NO; + + return [self XMLNode] == [other XMLNode] + || ([self kind] == [other kind] + && AreEqualOrBothNilPrivate([self name], [other name]) + && [[self children] count] == [[other children] count]); + +} + +- (NSUInteger)hash { + return (NSUInteger) (void *) [GDataXMLNode class]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { + return [super methodSignatureForSelector:selector]; +} + +#pragma mark - + +- (xmlNodePtr)XMLNodeCopy { + if (xmlNode_ != NULL) { + + // Note: libxml will create a new copy of namespace nodes (xmlNs records) + // and attach them to this copy in order to keep namespaces within this + // node subtree copy value. + + xmlNodePtr nodeCopy = xmlCopyNode(xmlNode_, 1); // 1 = recursive + return nodeCopy; + } + return NULL; +} + +- (xmlNodePtr)XMLNode { + return xmlNode_; +} + +- (BOOL)shouldFreeXMLNode { + return shouldFreeXMLNode_; +} + +- (void)setShouldFreeXMLNode:(BOOL)flag { + shouldFreeXMLNode_ = flag; +} + +@end + + + +@implementation GDataXMLElement + +- (id)initWithXMLString:(NSString *)str error:(NSError **)error { + self = [super init]; + if (self) { + + const char *utf8Str = [str UTF8String]; + // NOTE: We are assuming a string length that fits into an int + xmlDocPtr doc = xmlReadMemory(utf8Str, (int)strlen(utf8Str), NULL, // URL + NULL, // encoding + kGDataXMLParseOptions); + if (doc == NULL) { + if (error) { + // TODO(grobbins) use xmlSetGenericErrorFunc to capture error + } + } else { + // copy the root node from the doc + xmlNodePtr root = xmlDocGetRootElement(doc); + if (root) { + xmlNode_ = xmlCopyNode(root, 1); // 1: recursive + shouldFreeXMLNode_ = YES; + } + xmlFreeDoc(doc); + } + + + if (xmlNode_ == NULL) { + // failure + if (error) { + *error = [NSError errorWithDomain:@"com.google.GDataXML" + code:-1 + userInfo:nil]; + } + [self release]; + return nil; + } + } + return self; +} + +- (NSArray *)namespaces { + + NSMutableArray *array = nil; + + if (xmlNode_ != NULL && xmlNode_->nsDef != NULL) { + + xmlNsPtr currNS = xmlNode_->nsDef; + while (currNS != NULL) { + + // add this prefix/URI to the list, unless it's the implicit xml prefix + if (!xmlStrEqual(currNS->prefix, (const xmlChar *) "xml")) { + GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:(xmlNodePtr) currNS]; + + if (array == nil) { + array = [NSMutableArray arrayWithObject:node]; + } else { + [array addObject:node]; + } + } + + currNS = currNS->next; + } + } + return array; +} + +- (void)setNamespaces:(NSArray *)namespaces { + + if (xmlNode_ != NULL) { + + [self releaseCachedValues]; + + // remove previous namespaces + if (xmlNode_->nsDef) { + xmlFreeNsList(xmlNode_->nsDef); + xmlNode_->nsDef = NULL; + } + + // add a namespace for each object in the array + NSEnumerator *enumerator = [namespaces objectEnumerator]; + GDataXMLNode *namespaceNode; + while ((namespaceNode = [enumerator nextObject]) != nil) { + + xmlNsPtr ns = (xmlNsPtr) [namespaceNode XMLNode]; + if (ns) { + (void)xmlNewNs(xmlNode_, ns->href, ns->prefix); + } + } + + // we may need to fix this node's own name; the graft point is where + // the namespace search starts, so that points to this node too + [[self class] fixUpNamespacesForNode:xmlNode_ + graftingToTreeNode:xmlNode_]; + } +} + +- (void)addNamespace:(GDataXMLNode *)aNamespace { + + if (xmlNode_ != NULL) { + + [self releaseCachedValues]; + + xmlNsPtr ns = (xmlNsPtr) [aNamespace XMLNode]; + if (ns) { + (void)xmlNewNs(xmlNode_, ns->href, ns->prefix); + + // we may need to fix this node's own name; the graft point is where + // the namespace search starts, so that points to this node too + [[self class] fixUpNamespacesForNode:xmlNode_ + graftingToTreeNode:xmlNode_]; + } + } +} + +- (void)addChild:(GDataXMLNode *)child { + if ([child kind] == GDataXMLAttributeKind) { + [self addAttribute:child]; + return; + } + + if (xmlNode_ != NULL) { + + [self releaseCachedValues]; + + xmlNodePtr childNodeCopy = [child XMLNodeCopy]; + if (childNodeCopy) { + + xmlNodePtr resultNode = xmlAddChild(xmlNode_, childNodeCopy); + if (resultNode == NULL) { + + // failed to add + xmlFreeNode(childNodeCopy); + + } else { + // added this child subtree successfully; see if it has + // previously-unresolved namespace prefixes that can now be fixed up + [[self class] fixUpNamespacesForNode:childNodeCopy + graftingToTreeNode:xmlNode_]; + } + } + } +} + +- (void)removeChild:(GDataXMLNode *)child { + // this is safe for attributes too + if (xmlNode_ != NULL) { + + [self releaseCachedValues]; + + xmlNodePtr node = [child XMLNode]; + + xmlUnlinkNode(node); + + // if the child node was borrowing its xmlNodePtr, then we need to + // explicitly free it, since there is probably no owning object that will + // free it on dealloc + if (![child shouldFreeXMLNode]) { + xmlFreeNode(node); + } + } +} + +- (NSArray *)elementsForName:(NSString *)name { + + NSString *desiredName = name; + + if (xmlNode_ != NULL) { + + NSString *prefix = [[self class] prefixForName:desiredName]; + if (prefix) { + + xmlChar* desiredPrefix = GDataGetXMLString(prefix); + + xmlNsPtr foundNS = xmlSearchNs(xmlNode_->doc, xmlNode_, desiredPrefix); + if (foundNS) { + + // we found a namespace; fall back on elementsForLocalName:URI: + // to get the elements + NSString *desiredURI = [self stringFromXMLString:(foundNS->href)]; + NSString *localName = [[self class] localNameForName:desiredName]; + + NSArray *nsArray = [self elementsForLocalName:localName URI:desiredURI]; + return nsArray; + } + } + + // no namespace found for the node's prefix; try an exact match + // for the name argument, including any prefix + NSMutableArray *array = nil; + + // walk our list of cached child nodes + NSArray *children = [self children]; + + for (GDataXMLNode *child in children) { + + xmlNodePtr currNode = [child XMLNode]; + + // find all children which are elements with the desired name + if (currNode->type == XML_ELEMENT_NODE) { + + NSString *qName = [child name]; + if ([qName isEqual:name]) { + + if (array == nil) { + array = [NSMutableArray arrayWithObject:child]; + } else { + [array addObject:child]; + } + } + } + } + return array; + } + return nil; +} + +- (NSArray *)elementsForLocalName:(NSString *)localName URI:(NSString *)URI { + + NSMutableArray *array = nil; + + if (xmlNode_ != NULL && xmlNode_->children != NULL) { + + xmlChar* desiredNSHref = GDataGetXMLString(URI); + xmlChar* requestedLocalName = GDataGetXMLString(localName); + xmlChar* expectedLocalName = requestedLocalName; + + // resolve the URI at the parent level, since usually children won't + // have their own namespace definitions, and we don't want to try to + // resolve it once for every child + xmlNsPtr foundParentNS = xmlSearchNsByHref(xmlNode_->doc, xmlNode_, desiredNSHref); + if (foundParentNS == NULL) { + NSString *fakeQName = GDataFakeQNameForURIAndName(URI, localName); + expectedLocalName = GDataGetXMLString(fakeQName); + } + + NSArray *children = [self children]; + + for (GDataXMLNode *child in children) { + + xmlNodePtr currChildPtr = [child XMLNode]; + + // find all children which are elements with the desired name and + // namespace, or with the prefixed name and a null namespace + if (currChildPtr->type == XML_ELEMENT_NODE) { + + // normally, we can assume the resolution done for the parent will apply + // to the child, as most children do not define their own namespaces + xmlNsPtr childLocalNS = foundParentNS; + xmlChar* childDesiredLocalName = expectedLocalName; + + if (currChildPtr->nsDef != NULL) { + // this child has its own namespace definitons; do a fresh resolve + // of the namespace starting from the child, and see if it differs + // from the resolve done starting from the parent. If the resolve + // finds a different namespace, then override the desired local + // name just for this child. + childLocalNS = xmlSearchNsByHref(xmlNode_->doc, currChildPtr, desiredNSHref); + if (childLocalNS != foundParentNS) { + + // this child does indeed have a different namespace resolution + // result than was found for its parent + if (childLocalNS == NULL) { + // no namespace found + NSString *fakeQName = GDataFakeQNameForURIAndName(URI, localName); + childDesiredLocalName = GDataGetXMLString(fakeQName); + } else { + // a namespace was found; use the original local name requested, + // not a faked one expected from resolving the parent + childDesiredLocalName = requestedLocalName; + } + } + } + + // check if this child's namespace and local name are what we're + // seeking + if (currChildPtr->ns == childLocalNS + && currChildPtr->name != NULL + && xmlStrEqual(currChildPtr->name, childDesiredLocalName)) { + + if (array == nil) { + array = [NSMutableArray arrayWithObject:child]; + } else { + [array addObject:child]; + } + } + } + } + // we return nil, not an empty array, according to docs + } + return array; +} + +- (NSArray *)attributes { + + if (cachedAttributes_ != nil) { + return cachedAttributes_; + } + + NSMutableArray *array = nil; + + if (xmlNode_ != NULL && xmlNode_->properties != NULL) { + + xmlAttrPtr prop = xmlNode_->properties; + while (prop != NULL) { + + GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:(xmlNodePtr) prop]; + if (array == nil) { + array = [NSMutableArray arrayWithObject:node]; + } else { + [array addObject:node]; + } + + prop = prop->next; + } + + cachedAttributes_ = [array retain]; + } + return array; +} + +- (void)addAttribute:(GDataXMLNode *)attribute { + + if (xmlNode_ != NULL) { + + [self releaseCachedValues]; + + xmlAttrPtr attrPtr = (xmlAttrPtr) [attribute XMLNode]; + if (attrPtr) { + + // ignore this if an attribute with the name is already present, + // similar to NSXMLNode's addAttribute + xmlAttrPtr oldAttr; + + if (attrPtr->ns == NULL) { + oldAttr = xmlHasProp(xmlNode_, attrPtr->name); + } else { + oldAttr = xmlHasNsProp(xmlNode_, attrPtr->name, attrPtr->ns->href); + } + + if (oldAttr == NULL) { + + xmlNsPtr newPropNS = NULL; + + // if this attribute has a namespace, search for a matching namespace + // on the node we're adding to + if (attrPtr->ns != NULL) { + + newPropNS = xmlSearchNsByHref(xmlNode_->doc, xmlNode_, attrPtr->ns->href); + if (newPropNS == NULL) { + // make a new namespace on the parent node, and use that for the + // new attribute + newPropNS = xmlNewNs(xmlNode_, attrPtr->ns->href, attrPtr->ns->prefix); + } + } + + // copy the attribute onto this node + xmlChar *value = xmlNodeGetContent((xmlNodePtr) attrPtr); + xmlAttrPtr newProp = xmlNewNsProp(xmlNode_, newPropNS, attrPtr->name, value); + if (newProp != NULL) { + // we made the property, so clean up the property's namespace + + [[self class] fixUpNamespacesForNode:(xmlNodePtr)newProp + graftingToTreeNode:xmlNode_]; + } + + if (value != NULL) { + xmlFree(value); + } + } + } + } +} + +- (GDataXMLNode *)attributeForXMLNode:(xmlAttrPtr)theXMLNode { + // search the cached attributes list for the GDataXMLNode with + // the underlying xmlAttrPtr + NSArray *attributes = [self attributes]; + + for (GDataXMLNode *attr in attributes) { + + if (theXMLNode == (xmlAttrPtr) [attr XMLNode]) { + return attr; + } + } + + return nil; +} + +- (GDataXMLNode *)attributeForName:(NSString *)name { + + if (xmlNode_ != NULL) { + + xmlAttrPtr attrPtr = xmlHasProp(xmlNode_, GDataGetXMLString(name)); + if (attrPtr == NULL) { + + // can we guarantee that xmlAttrPtrs always have the ns ptr and never + // a namespace as part of the actual attribute name? + + // split the name and its prefix, if any + xmlNsPtr ns = NULL; + NSString *prefix = [[self class] prefixForName:name]; + if (prefix) { + + // find the namespace for this prefix, and search on its URI to find + // the xmlNsPtr + name = [[self class] localNameForName:name]; + ns = xmlSearchNs(xmlNode_->doc, xmlNode_, GDataGetXMLString(prefix)); + } + + const xmlChar* nsURI = ((ns != NULL) ? ns->href : NULL); + attrPtr = xmlHasNsProp(xmlNode_, GDataGetXMLString(name), nsURI); + } + + if (attrPtr) { + GDataXMLNode *attr = [self attributeForXMLNode:attrPtr]; + return attr; + } + } + return nil; +} + +- (GDataXMLNode *)attributeForLocalName:(NSString *)localName + URI:(NSString *)attributeURI { + + if (xmlNode_ != NULL) { + + const xmlChar* name = GDataGetXMLString(localName); + const xmlChar* nsURI = GDataGetXMLString(attributeURI); + + xmlAttrPtr attrPtr = xmlHasNsProp(xmlNode_, name, nsURI); + + if (attrPtr == NULL) { + // if the attribute is in a tree lacking the proper namespace, + // the local name may include the full URI as a prefix + NSString *fakeQName = GDataFakeQNameForURIAndName(attributeURI, localName); + const xmlChar* xmlFakeQName = GDataGetXMLString(fakeQName); + + attrPtr = xmlHasProp(xmlNode_, xmlFakeQName); + } + + if (attrPtr) { + GDataXMLNode *attr = [self attributeForXMLNode:attrPtr]; + return attr; + } + } + return nil; +} + +- (NSString *)resolvePrefixForNamespaceURI:(NSString *)namespaceURI { + + if (xmlNode_ != NULL) { + + xmlChar* desiredNSHref = GDataGetXMLString(namespaceURI); + + xmlNsPtr foundNS = xmlSearchNsByHref(xmlNode_->doc, xmlNode_, desiredNSHref); + if (foundNS) { + + // we found the namespace + if (foundNS->prefix != NULL) { + NSString *prefix = [self stringFromXMLString:(foundNS->prefix)]; + return prefix; + } else { + // empty prefix is default namespace + return @""; + } + } + } + return nil; +} + +#pragma mark Namespace fixup routines + ++ (void)deleteNamespacePtr:(xmlNsPtr)namespaceToDelete + fromXMLNode:(xmlNodePtr)node { + + // utilty routine to remove a namespace pointer from an element's + // namespace definition list. This is just removing the nsPtr + // from the singly-linked list, the node's namespace definitions. + xmlNsPtr currNS = node->nsDef; + xmlNsPtr prevNS = NULL; + + while (currNS != NULL) { + xmlNsPtr nextNS = currNS->next; + + if (namespaceToDelete == currNS) { + + // found it; delete it from the head of the node's ns definition list + // or from the next field of the previous namespace + + if (prevNS != NULL) prevNS->next = nextNS; + else node->nsDef = nextNS; + + xmlFreeNs(currNS); + return; + } + prevNS = currNS; + currNS = nextNS; + } +} + ++ (void)fixQualifiedNamesForNode:(xmlNodePtr)nodeToFix + graftingToTreeNode:(xmlNodePtr)graftPointNode { + + // Replace prefix-in-name with proper namespace pointers + // + // This is an inner routine for fixUpNamespacesForNode: + // + // see if this node's name lacks a namespace and is qualified, and if so, + // see if we can resolve the prefix against the parent + // + // The prefix may either be normal, "gd:foo", or a URI + // "{http://blah.com/}:foo" + + if (nodeToFix->ns == NULL) { + xmlNsPtr foundNS = NULL; + + xmlChar* prefix = NULL; + xmlChar* localName = SplitQNameReverse(nodeToFix->name, &prefix); + if (localName != NULL) { + if (prefix != NULL) { + + // if the prefix is wrapped by { and } then it's a URI + int prefixLen = xmlStrlen(prefix); + if (prefixLen > 2 + && prefix[0] == '{' + && prefix[prefixLen - 1] == '}') { + + // search for the namespace by URI + xmlChar* uri = xmlStrsub(prefix, 1, prefixLen - 2); + + if (uri != NULL) { + foundNS = xmlSearchNsByHref(graftPointNode->doc, graftPointNode, uri); + + xmlFree(uri); + } + } + } + + if (foundNS == NULL) { + // search for the namespace by prefix, even if the prefix is nil + // (nil prefix means to search for the default namespace) + foundNS = xmlSearchNs(graftPointNode->doc, graftPointNode, prefix); + } + + if (foundNS != NULL) { + // we found a namespace, so fix the ns pointer and the local name + xmlSetNs(nodeToFix, foundNS); + xmlNodeSetName(nodeToFix, localName); + } + + if (prefix != NULL) { + xmlFree(prefix); + prefix = NULL; + } + + xmlFree(localName); + } + } +} + ++ (void)fixDuplicateNamespacesForNode:(xmlNodePtr)nodeToFix + graftingToTreeNode:(xmlNodePtr)graftPointNode + namespaceSubstitutionMap:(NSMutableDictionary *)nsMap { + + // Duplicate namespace removal + // + // This is an inner routine for fixUpNamespacesForNode: + // + // If any of this node's namespaces are already defined at the graft point + // level, add that namespace to the map of namespace substitutions + // so it will be replaced in the children below the nodeToFix, and + // delete the namespace record + + if (nodeToFix->type == XML_ELEMENT_NODE) { + + // step through the namespaces defined on this node + xmlNsPtr definedNS = nodeToFix->nsDef; + while (definedNS != NULL) { + + // see if this namespace is already defined higher in the tree, + // with both the same URI and the same prefix; if so, add a mapping for + // it + xmlNsPtr foundNS = xmlSearchNsByHref(graftPointNode->doc, graftPointNode, + definedNS->href); + if (foundNS != NULL + && foundNS != definedNS + && xmlStrEqual(definedNS->prefix, foundNS->prefix)) { + + // store a mapping from this defined nsPtr to the one found higher + // in the tree + [nsMap setObject:[NSValue valueWithPointer:foundNS] + forKey:[NSValue valueWithPointer:definedNS]]; + + // remove this namespace from the ns definition list of this node; + // all child elements and attributes referencing this namespace + // now have a dangling pointer and must be updated (that is done later + // in this method) + // + // before we delete this namespace, move our pointer to the + // next one + xmlNsPtr nsToDelete = definedNS; + definedNS = definedNS->next; + + [self deleteNamespacePtr:nsToDelete fromXMLNode:nodeToFix]; + + } else { + // this namespace wasn't a duplicate; move to the next + definedNS = definedNS->next; + } + } + } + + // if this node's namespace is one we deleted, update it to point + // to someplace better + if (nodeToFix->ns != NULL) { + + NSValue *currNS = [NSValue valueWithPointer:nodeToFix->ns]; + NSValue *replacementNS = [nsMap objectForKey:currNS]; + + if (replacementNS != nil) { + xmlNsPtr replaceNSPtr = (xmlNsPtr)[replacementNS pointerValue]; + + xmlSetNs(nodeToFix, replaceNSPtr); + } + } +} + + + ++ (void)fixUpNamespacesForNode:(xmlNodePtr)nodeToFix + graftingToTreeNode:(xmlNodePtr)graftPointNode + namespaceSubstitutionMap:(NSMutableDictionary *)nsMap { + + // This is the inner routine for fixUpNamespacesForNode:graftingToTreeNode: + // + // This routine fixes two issues: + // + // Because we can create nodes with qualified names before adding + // them to the tree that declares the namespace for the prefix, + // we need to set the node namespaces after adding them to the tree. + // + // Because libxml adds namespaces to nodes when it copies them, + // we want to remove redundant namespaces after adding them to + // a tree. + // + // If only the Mac's libxml had xmlDOMWrapReconcileNamespaces, it could do + // namespace cleanup for us + + // We only care about fixing names of elements and attributes + if (nodeToFix->type != XML_ELEMENT_NODE + && nodeToFix->type != XML_ATTRIBUTE_NODE) return; + + // Do the fixes + [self fixQualifiedNamesForNode:nodeToFix + graftingToTreeNode:graftPointNode]; + + [self fixDuplicateNamespacesForNode:nodeToFix + graftingToTreeNode:graftPointNode + namespaceSubstitutionMap:nsMap]; + + if (nodeToFix->type == XML_ELEMENT_NODE) { + + // when fixing element nodes, recurse for each child element and + // for each attribute + xmlNodePtr currChild = nodeToFix->children; + while (currChild != NULL) { + [self fixUpNamespacesForNode:currChild + graftingToTreeNode:graftPointNode + namespaceSubstitutionMap:nsMap]; + currChild = currChild->next; + } + + xmlAttrPtr currProp = nodeToFix->properties; + while (currProp != NULL) { + [self fixUpNamespacesForNode:(xmlNodePtr)currProp + graftingToTreeNode:graftPointNode + namespaceSubstitutionMap:nsMap]; + currProp = currProp->next; + } + } +} + ++ (void)fixUpNamespacesForNode:(xmlNodePtr)nodeToFix + graftingToTreeNode:(xmlNodePtr)graftPointNode { + + // allocate the namespace map that will be passed + // down on recursive calls + NSMutableDictionary *nsMap = [NSMutableDictionary dictionary]; + + [self fixUpNamespacesForNode:nodeToFix + graftingToTreeNode:graftPointNode + namespaceSubstitutionMap:nsMap]; +} + +@end + + +@interface GDataXMLDocument (PrivateMethods) +- (void)addStringsCacheToDoc; +@end + +@implementation GDataXMLDocument + +- (id)initWithXMLString:(NSString *)str options:(unsigned int)mask error:(NSError **)error { + + NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding]; + GDataXMLDocument *doc = [self initWithData:data options:mask error:error]; + return doc; +} + +- (id)initWithData:(NSData *)data options:(unsigned int)mask error:(NSError **)error { + + self = [super init]; + if (self) { + + const char *baseURL = NULL; + const char *encoding = NULL; + + // NOTE: We are assuming [data length] fits into an int. + xmlDoc_ = xmlReadMemory((const char*)[data bytes], (int)[data length], baseURL, encoding, + kGDataXMLParseOptions); // TODO(grobbins) map option values + if (xmlDoc_ == NULL) { + if (error) { + *error = [NSError errorWithDomain:@"com.google.GDataXML" + code:-1 + userInfo:nil]; + // TODO(grobbins) use xmlSetGenericErrorFunc to capture error + } + [self release]; + return nil; + } else { + if (error) *error = NULL; + + [self addStringsCacheToDoc]; + } + } + + return self; +} + +- (id)initWithRootElement:(GDataXMLElement *)element { + + self = [super init]; + if (self) { + + xmlDoc_ = xmlNewDoc(NULL); + + (void) xmlDocSetRootElement(xmlDoc_, [element XMLNodeCopy]); + + [self addStringsCacheToDoc]; + } + + return self; +} + +- (void)addStringsCacheToDoc { + // utility routine for init methods + +#if DEBUG + NSCAssert(xmlDoc_ != NULL && xmlDoc_->_private == NULL, + @"GDataXMLDocument cache creation problem"); +#endif + + // add a strings cache as private data for the document + // + // we'll use plain C pointers (xmlChar*) as the keys, and NSStrings + // as the values + CFIndex capacity = 0; // no limit + + CFDictionaryKeyCallBacks keyCallBacks = { + 0, // version + StringCacheKeyRetainCallBack, + StringCacheKeyReleaseCallBack, + StringCacheKeyCopyDescriptionCallBack, + StringCacheKeyEqualCallBack, + StringCacheKeyHashCallBack + }; + + CFMutableDictionaryRef dict = CFDictionaryCreateMutable( + kCFAllocatorDefault, capacity, + &keyCallBacks, &kCFTypeDictionaryValueCallBacks); + + // we'll use the user-defined _private field for our cache + xmlDoc_->_private = dict; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %p", [self class], self]; +} + +- (void)dealloc { + if (xmlDoc_ != NULL) { + // release the strings cache + // + // since it's a CF object, were anyone to use this in a GC environment, + // this would need to be released in a finalize method, too + if (xmlDoc_->_private != NULL) { + CFRelease(xmlDoc_->_private); + } + + xmlFreeDoc(xmlDoc_); + } + [super dealloc]; +} + +#pragma mark - + +- (GDataXMLElement *)rootElement { + GDataXMLElement *element = nil; + + if (xmlDoc_ != NULL) { + xmlNodePtr rootNode = xmlDocGetRootElement(xmlDoc_); + if (rootNode) { + element = [GDataXMLElement nodeBorrowingXMLNode:rootNode]; + } + } + return element; +} + +- (NSData *)XMLData { + + if (xmlDoc_ != NULL) { + xmlChar *buffer = NULL; + int bufferSize = 0; + +// xmlDocDumpMemory(xmlDoc_, &buffer, &bufferSize); + xmlDocDumpMemoryEnc(xmlDoc_, &buffer, &bufferSize, "utf-8"); + + if (buffer) { + NSData *data = [NSData dataWithBytes:buffer + length:(NSUInteger)bufferSize]; + xmlFree(buffer); + return data; + } + } + return nil; +} + +- (void)setVersion:(NSString *)version { + + if (xmlDoc_ != NULL) { + if (xmlDoc_->version != NULL) { + // version is a const char* so we must cast + xmlFree((char *) xmlDoc_->version); + xmlDoc_->version = NULL; + } + + if (version != nil) { + xmlDoc_->version = xmlStrdup(GDataGetXMLString(version)); + } + } +} + +- (void)setCharacterEncoding:(NSString *)encoding { + + if (xmlDoc_ != NULL) { + if (xmlDoc_->encoding != NULL) { + // version is a const char* so we must cast + xmlFree((char *) xmlDoc_->encoding); + xmlDoc_->encoding = NULL; + } + + if (encoding != nil) { + xmlDoc_->encoding = xmlStrdup(GDataGetXMLString(encoding)); + } + } +} + +- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error { + return [self nodesForXPath:xpath namespaces:nil error:error]; +} + +- (NSArray *)nodesForXPath:(NSString *)xpath + namespaces:(NSDictionary *)namespaces + error:(NSError **)error { + if (xmlDoc_ != NULL) { + GDataXMLNode *docNode = [GDataXMLElement nodeBorrowingXMLNode:(xmlNodePtr)xmlDoc_]; + NSArray *array = [docNode nodesForXPath:xpath + namespaces:namespaces + error:error]; + return array; + } + return nil; +} + +@end + +// +// Dictionary key callbacks for our C-string to NSString cache dictionary +// +static const void *StringCacheKeyRetainCallBack(CFAllocatorRef allocator, const void *str) { + // copy the key + xmlChar* key = xmlStrdup(str); + return key; +} + +static void StringCacheKeyReleaseCallBack(CFAllocatorRef allocator, const void *str) { + // free the key + char *chars = (char *)str; + xmlFree((char *) chars); +} + +static CFStringRef StringCacheKeyCopyDescriptionCallBack(const void *str) { + // make a CFString from the key + CFStringRef cfStr = CFStringCreateWithCString(kCFAllocatorDefault, + (const char *)str, + kCFStringEncodingUTF8); + return cfStr; +} + +static Boolean StringCacheKeyEqualCallBack(const void *str1, const void *str2) { + // compare the key strings + if (str1 == str2) return true; + + int result = xmlStrcmp(str1, str2); + return (result == 0); +} + +static CFHashCode StringCacheKeyHashCallBack(const void *str) { + + // dhb hash, per http://www.cse.yorku.ca/~oz/hash.html + CFHashCode hash = 5381; + unsigned int c; + const unsigned char *chars = (const unsigned char *)str; + + while ((c = *chars++) != 0) { + hash = ((hash << 5) + hash) + c; + } + return hash; +} diff --git a/AppleParty/AppleParty/Vendors/ITMS/XMLManager.swift b/AppleParty/AppleParty/Vendors/ITMS/XMLManager.swift new file mode 100644 index 0000000..63df68d --- /dev/null +++ b/AppleParty/AppleParty/Vendors/ITMS/XMLManager.swift @@ -0,0 +1,101 @@ +// +// XMLManager.swift +// AppleParty +// +// Created by 易承 on 2021/5/25. +// + +import Foundation + +class XMLManager { + + static let appPtah = "/AppleParty/InAppPurches/" + static let shotPtah = "/AppleParty/ScreenShots/" + static let ipaPtah = "/AppleParty/UploadIpa/" + + static func getITMSPath(_ appid: String) -> String { + return getFilePath(appid, filePath: appPtah) + } + + static func getShotsPath(_ appid: String) -> String { + return getFilePath(appid, filePath: shotPtah) + } + + static func getIpaPath(_ appid: String) -> String { + return getFilePath(appid, filePath: ipaPtah) + } + + static func getFilePath(_ appid: String, filePath: String) -> String { + let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) + let documentsDirectory = paths[0] + let filePath = documentsDirectory + filePath + appid + ".itmsp" + return filePath + } + + static func verifyITMS(account: String, pwd: String, filePath: String) -> (Int32, String?) { + return runShellWithArgsAndOutput(launchPath: iTMSTransporter, "-f", filePath, "-m", "verify", "-u", account, "-p", pwd) + } + + static func uploadITMS(account: String, pwd: String, filePath: String) -> (Int32, String?) { + return runShellWithArgsAndOutput(launchPath: iTMSTransporter, "-f", filePath, "-m", "upload", "-u", account, "-p", pwd) + } + + static func deleteITMS(_ filePath:String) { + do { + let fileManager = FileManager.default + // Check if file or directory exists + if fileManager.fileExists(atPath: filePath) { + // Delete file or directory + try fileManager.removeItem(atPath: filePath) + } else { + print("File does not exist: \(filePath)") + } + } catch { + print("File An error took place: \(error)") + } + } + + static func openDocuments() { + let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) + let documentsDirectory = paths[0] + runShellWithArgs(launchPath: "open", documentsDirectory) + } + + static func copySimpleExel() { + if let xlsxPath = Bundle.main.path(forResource: "example", ofType: "xlsx") { + let dateFormatter : DateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMdd_HHmmss" + let currentDate = dateFormatter.string(from: Date()) + try? FileManager.default.copyItem(atPath: xlsxPath, toPath: NSHomeDirectory()+"/Desktop/IAP-\(currentDate).xlsx") + } + runShellWithArgs(launchPath: "/usr/bin/open", NSHomeDirectory()+"/Desktop/") + } +} + +// MARK: - Shell命令 +let iTMSTransporter = "/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter" +let shell = "/usr/bin/env" + +@discardableResult +func runShellWithArgs(launchPath: String, _ args: String...) -> Int32 { + let task = Process() + task.launchPath = launchPath + task.arguments = args + task.launch() + task.waitUntilExit() + return task.terminationStatus +} + +func runShellWithArgsAndOutput(launchPath: String, _ args: String...) -> (Int32, String?) { + let task = Process() + task.launchPath = launchPath + task.arguments = args + let pipe = Pipe() + task.standardOutput = pipe + task.standardError = pipe + task.launch() + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) + task.waitUntilExit() + return (task.terminationStatus, output) +} diff --git a/AppleParty/AppleParty/Vendors/MBProgressHUD-OSX/MBProgressHUD.h b/AppleParty/AppleParty/Vendors/MBProgressHUD-OSX/MBProgressHUD.h new file mode 100755 index 0000000..fb3a380 --- /dev/null +++ b/AppleParty/AppleParty/Vendors/MBProgressHUD-OSX/MBProgressHUD.h @@ -0,0 +1,748 @@ +// +// MBProgresHUD.h +// Created by vanelizarov © 2015 +// +// + +// This code is distributed under the terms and conditions of the MIT license. + +// Copyright © 2015 vanelizarov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +// refer: https://github.com/vanelizarov/MBProgressHUD-OSX.git + +#import +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + #import + #if __IPHONE + #import + #endif // __IPHONE +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + #import +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +@protocol MBProgressHUDDelegate; + +/* + NSProgressIndicator* indicator = [[[NSProgressIndicator alloc] initWithFrame:NSMakeRect(20, 20, 30, 30)] autorelease]; + [indicator setStyle:NSProgressIndicatorSpinningStyle]; + + https://developer.apple.com/library/mac/documentation/cocoa/conceptual/ProgIndic/Concepts/AboutProgIndic.html + + */ + +typedef enum { + /** Progress is shown using an UIActivityIndicatorView. This is the default. */ + MBProgressHUDModeIndeterminate, + /** Progress is shown using a round, pie-chart like, progress view. */ + MBProgressHUDModeDeterminate, + /** Progress is shown using a horizontal progress bar */ + MBProgressHUDModeDeterminateHorizontalBar, + /** Progress is shown using a ring-shaped progress view. */ + MBProgressHUDModeAnnularDeterminate, + /** Shows a custom view */ + MBProgressHUDModeCustomView, + /** Shows only labels */ + MBProgressHUDModeText +} MBProgressHUDMode; + +typedef enum { + /** Opacity animation */ + MBProgressHUDAnimationFade, + /** Opacity + scale animation */ + MBProgressHUDAnimationZoom, + MBProgressHUDAnimationZoomOut = MBProgressHUDAnimationZoom, + MBProgressHUDAnimationZoomIn +} MBProgressHUDAnimation; + + +#ifndef MB_INSTANCETYPE +#if __has_feature(objc_instancetype) +#define MB_INSTANCETYPE instancetype +#else +#define MB_INSTANCETYPE id +#endif +#endif + +#ifndef MB_STRONG +#if __has_feature(objc_arc) +#define MB_STRONG strong +#else +#define MB_STRONG retain +#endif +#endif + +#ifndef MB_WEAK +#if __has_feature(objc_arc_weak) +#define MB_WEAK weak +#elif __has_feature(objc_arc) +#define MB_WEAK unsafe_unretained +#else +#define MB_WEAK assign +#endif +#endif + +#if NS_BLOCKS_AVAILABLE +typedef void (^MBProgressHUDCompletionBlock)(); +#endif + +#if !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +enum { + NSViewAutoresizingNone = NSViewNotSizable, + NSViewAutoresizingFlexibleLeftMargin = NSViewMinXMargin, + NSViewAutoresizingFlexibleWidth = NSViewWidthSizable, + NSViewAutoresizingFlexibleRightMargin = NSViewMaxXMargin, + NSViewAutoresizingFlexibleTopMargin = NSViewMaxYMargin, + NSViewAutoresizingFlexibleHeight = NSViewHeightSizable, + NSViewAutoresizingFlexibleBottomMargin = NSViewMinYMargin +}; + +#endif + +/** + * Displays a simple HUD window containing a progress indicator and two optional labels for short messages. + * + * This is a simple drop-in class for displaying a progress HUD view similar to Apple's private UIProgressHUD class. + * The MBProgressHUD window spans over the entire space given to it by the initWithFrame constructor and catches all + * user input on this region, thereby preventing the user operations on components below the view. The HUD itself is + * drawn centered as a rounded semi-transparent view which resizes depending on the user specified content. + * + * This view supports four modes of operation: + * - MBProgressHUDModeIndeterminate - shows a UIActivityIndicatorView + * - MBProgressHUDModeDeterminate - shows a custom round progress indicator + * - MBProgressHUDModeAnnularDeterminate - shows a custom annular progress indicator + * - MBProgressHUDModeCustomView - shows an arbitrary, user specified view (@see customView) + * + * All three modes can have optional labels assigned: + * - If the labelText property is set and non-empty then a label containing the provided content is placed below the + * indicator view. + * - If also the detailsLabelText property is set then another label is placed below the first label. + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@interface MBProgressHUD : UIView +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@interface MBProgressHUD : NSView +{ + CGColorRef _cgColorFromNSColor; +} +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +#pragma mark - Class methods + +/** + * Creates a new HUD, adds it to provided view and shows it. The counterpart to this method is hideHUDForView:animated:. + * + * @param view The view that the HUD will be added to + * @param animated If set to YES the HUD will appear using the current animationType. If set to NO the HUD will not use + * animations while appearing. + * @return A reference to the created HUD. + * + * @see hideHUDForView:animated: + * @see animationType + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (MB_INSTANCETYPE)showHUDAddedTo:(UIView *)view animated:(BOOL)animated; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (MB_INSTANCETYPE)showHUDAddedTo:(NSView *)view animated:(BOOL)animated; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * Finds the top-most HUD subview and hides it. The counterpart to this method is showHUDAddedTo:animated:. + * + * @param view The view that is going to be searched for a HUD subview. + * @param animated If set to YES the HUD will disappear using the current animationType. If set to NO the HUD will not use + * animations while disappearing. + * @return YES if a HUD was found and removed, NO otherwise. + * + * @see showHUDAddedTo:animated: + * @see animationType + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (BOOL)hideHUDForView:(NSView *)view animated:(BOOL)animated; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * Finds all the HUD subviews and hides them. + * + * @param view The view that is going to be searched for HUD subviews. + * @param animated If set to YES the HUDs will disappear using the current animationType. If set to NO the HUDs will not use + * animations while disappearing. + * @return the number of HUDs found and removed. + * + * @see hideHUDForView:animated: + * @see animationType + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (NSUInteger)hideAllHUDsForView:(UIView *)view animated:(BOOL)animated; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (NSUInteger)hideAllHUDsForView:(NSView *)view animated:(BOOL)animated; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * Finds the top-most HUD subview and returns it. + * + * @param view The view that is going to be searched. + * @return A reference to the last HUD subview discovered. + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (MB_INSTANCETYPE)HUDForView:(UIView *)view; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (MB_INSTANCETYPE)HUDForView:(NSView *)view; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * Finds all HUD subviews and returns them. + * + * @param view The view that is going to be searched. + * @return All found HUD views (array of MBProgressHUD objects). + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (NSArray *)allHUDsForView:(UIView *)view; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (NSArray *)allHUDsForView:(NSView *)view; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * OS X Helper method to convert NSColor value to CGColorRef value. + * + * @nscolor NSColur instance to convert. + * @return Converted NSColor to CGColorRef. + */ +#if !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (CGColorRef)NSColorToCGColor:(NSColor *)nscolor; +#endif // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +#pragma mark - Lifecycle + +/** + * A convenience constructor that initializes the HUD with the window's bounds. Calls the designated constructor with + * window.bounds as the parameter. + * + * @param window The window instance that will provide the bounds for the HUD. Should be the same instance as + * the HUD's superview (i.e., the window that the HUD will be added to). + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (id)initWithWindow:(UIWindow *)window; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (id)initWithWindow:(NSWindow *)window; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * A convenience constructor that initializes the HUD with the view's bounds. Calls the designated constructor with + * view.bounds as the parameter + * + * @param view The view instance that will provide the bounds for the HUD. Should be the same instance as + * the HUD's superview (i.e., the view that the HUD will be added to). + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (id)initWithView:(UIView *)view; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (id)initWithView:(NSView *)view; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +#pragma mark - Show & hide + +/** + * Display the HUD. You need to make sure that the main thread completes its run loop soon after this method call so + * the user interface can be updated. Call this method when your task is already set-up to be executed in a new thread + * (e.g., when using something like NSOperation or calling an asynchronous call like NSURLRequest). + * + * @param animated If set to YES the HUD will appear using the current animationType. If set to NO the HUD will not use + * animations while appearing. + * + * @see animationType + */ +- (void)show:(BOOL)animated; + +/** + * Hide the HUD. This still calls the hudWasHidden: delegate. This is the counterpart of the show: method. Use it to + * hide the HUD when your task completes. + * + * @param animated If set to YES the HUD will disappear using the current animationType. If set to NO the HUD will not use + * animations while disappearing. + * + * @see animationType + */ +- (void)hide:(BOOL)animated; + +/** + * Hide the HUD after a delay. This still calls the hudWasHidden: delegate. This is the counterpart of the show: method. Use it to + * hide the HUD when your task completes. + * + * @param animated If set to YES the HUD will disappear using the current animationType. If set to NO the HUD will not use + * animations while disappearing. + * @param delay Delay in seconds until the HUD is hidden. + * + * @see animationType + */ +- (void)hide:(BOOL)animated afterDelay:(NSTimeInterval)delay; + +/** + * Read Only method that returns TRUE if HUD is no longer visible + */ +- (BOOL)isFinished; + +#pragma mark - Threading + +/** + * Shows the HUD while a background task is executing in a new thread, then hides the HUD. + * + * This method also takes care of autorelease pools so your method does not have to be concerned with setting up a + * pool. + * + * @param method The method to be executed while the HUD is shown. This method will be executed in a new thread. + * @param target The object that the target method belongs to. + * @param object An optional object to be passed to the method. + * @param animated If set to YES the HUD will (dis)appear using the current animationType. If set to NO the HUD will not use + * animations while (dis)appearing. + */ +- (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated; + +#if NS_BLOCKS_AVAILABLE + +/** + * Shows the HUD while a block is executing on a background queue, then hides the HUD. + * + * @see showAnimated:whileExecutingBlock:onQueue:completionBlock: + */ +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block; + +/** + * Shows the HUD while a block is executing on a background queue, then hides the HUD. + * + * @see showAnimated:whileExecutingBlock:onQueue:completionBlock: + */ +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block completionBlock:(MBProgressHUDCompletionBlock)completion; + +/** + * Shows the HUD while a block is executing on the specified dispatch queue, then hides the HUD. + * + * @see showAnimated:whileExecutingBlock:onQueue:completionBlock: + */ +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue; + +/** + * Shows the HUD while a block is executing on the specified dispatch queue, executes completion block on the main queue, and then hides the HUD. + * + * @param animated If set to YES the HUD will (dis)appear using the current animationType. If set to NO the HUD will + * not use animations while (dis)appearing. + * @param block The block to be executed while the HUD is shown. + * @param queue The dispatch queue on which the block should be executed. + * @param completion The block to be executed on completion. + * + * @see completionBlock + */ +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue + completionBlock:(MBProgressHUDCompletionBlock)completion; + +#pragma mark - Properties + +/** + * A block that gets called after the HUD was completely hidden. + */ +@property (copy) MBProgressHUDCompletionBlock completionBlock; + +#endif // NS_BLOCKS_AVAILABLE + +/** + * MBProgressHUD operation mode. The default is MBProgressHUDModeIndeterminate. + * + * @see MBProgressHUDMode + */ +@property (assign) MBProgressHUDMode mode; + +/** + * The animation type that should be used when the HUD is shown and hidden. + * + * @see MBProgressHUDAnimation + */ +@property (assign) MBProgressHUDAnimation animationType; + +/** + * The UIView (e.g., a UIImageView) to be shown when the HUD is in MBProgressHUDModeCustomView. + * For best results use a 37 by 37 pixel view (so the bounds match the built in indicator bounds). + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (MB_STRONG) UIView *customView; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (MB_STRONG) NSView *customView; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * The HUD delegate object. + * + * @see MBProgressHUDDelegate + */ +@property (MB_WEAK) id delegate; + +/** + * An optional short message to be displayed below the activity indicator. The HUD is automatically resized to fit + * the entire text. If the text is too long it will get clipped by displaying "..." at the end. If left unchanged or + * set to @"", then no message is displayed. + */ +@property (copy) NSString *labelText; + +/** + * An optional details message displayed below the labelText message. This message is displayed only if the labelText + * property is also set and is different from an empty string (@""). The details text can span multiple lines. + */ +@property (copy) NSString *detailsLabelText; + +/** + * The opacity of the HUD window. Defaults to 0.8 (80% opacity). + */ +@property (assign) float opacity; + +/** + * The color of the HUD window. Defaults to black. If this property is set, color is set using + * this UIColor and the opacity property is not used. using retain because performing copy on + * UIColor base colors (like [UIColor greenColor]) cause problems with the copyZone. + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (MB_STRONG) UIColor *color; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (MB_STRONG) NSColor *color; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +/** + * The x-axis offset of the HUD relative to the centre of the superview. + */ +@property (assign) float xOffset; + +/** + * The y-axis offset of the HUD relative to the centre of the superview. + */ +@property (assign) float yOffset; + +/** + * The size both horizontally and vertically of the spinner + * Defaults to 37.0 on iOS and to 60.0 for Mac OS X + */ +@property (assign) float spinsize; + +/** + * The amount of space between the HUD edge and the HUD elements (labels, indicators or custom views). + * Defaults to 20.0 + */ +@property (assign) float margin; + +/** + * The corner radius for th HUD + * Defaults to 10.0 + */ +@property (assign) float cornerRadius; + +/** + * Cover the HUD background view with a radial gradient. + */ +@property (assign) BOOL dimBackground; + +/** + * Allow User to dismiss HUD manually by a tap event. This calls the optional hudWasTapped: delegate. + * Defaults to NO. + */ +@property (assign) BOOL dismissible; + + +/* + * Grace period is the time (in seconds) that the invoked method may be run without + * showing the HUD. If the task finishes before the grace time runs out, the HUD will + * not be shown at all. + * This may be used to prevent HUD display for very short tasks. + * Defaults to 0 (no grace time). + * Grace time functionality is only supported when the task status is known! + * @see taskInProgress + */ +@property (assign) float graceTime; + +/** + * The minimum time (in seconds) that the HUD is shown. + * This avoids the problem of the HUD being shown and than instantly hidden. + * Defaults to 0 (no minimum show time). + */ +@property (assign) float minShowTime; + +/** + * Indicates that the executed operation is in progress. Needed for correct graceTime operation. + * If you don't set a graceTime (different than 0.0) this does nothing. + * This property is automatically set when using showWhileExecuting:onTarget:withObject:animated:. + * When threading is done outside of the HUD (i.e., when the show: and hide: methods are used directly), + * you need to set this property when your task starts and completes in order to have normal graceTime + * functionality. + */ +@property (assign) BOOL taskInProgress; + +/** + * Removes the HUD from its parent view when hidden. + * Defaults to NO. + */ +@property (assign) BOOL removeFromSuperViewOnHide; + +/** + * Font to be used for the main label. Set this property if the default is not adequate. + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (MB_STRONG) UIFont* labelFont; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (MB_STRONG) NSFont* labelFont; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * Color to be used for the main label. Set this property if the default is not adequate. + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (MB_STRONG) UIColor* labelColor; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (MB_STRONG) NSColor* labelColor; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * Font to be used for the details label. Set this property if the default is not adequate. + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (MB_STRONG) UIFont* detailsLabelFont; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (MB_STRONG) NSFont* detailsLabelFont; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * Color to be used for the details label. Set this property if the default is not adequate. + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (MB_STRONG) UIColor* detailsLabelColor; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (MB_STRONG) NSColor* detailsLabelColor; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * The progress of the progress indicator, from 0.0 to 1.0. Defaults to 0.0. + */ +@property (assign) float progress; + +/** + * The minimum size of the HUD bezel. Defaults to CGSizeZero (no minimum size). + */ +@property (assign) CGSize minSize; + +/** + * Force the HUD dimensions to be equal if possible. + */ +@property (assign, getter = isSquare) BOOL square; + +@end + + +@protocol MBProgressHUDDelegate + +@optional + +/** + * Called after the HUD was fully hidden from the screen. + */ +- (void)hudWasHidden:(MBProgressHUD *)hud; + +/** + * Called after the HUD delay timed out but before HUD was fully hidden from the screen. + */ +- (void)hudWasHiddenAfterDelay:(MBProgressHUD *)hud; + +/** + * Called after the HUD was Tapped with dismissible option enabled. + */ +- (void)hudWasTapped:(MBProgressHUD *)hud; + +/** + * OS X Helper method to convert NSColor value to CGColorRef value. + * + * @nscolor NSColur instance to convert. + * @return Converted NSColor to CGColorRef. + */ +#if !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (CGColorRef)NSColorToCGColor:(NSColor *)nscolor; +#endif // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +@end + + +/** + * A progress view for showing definite progress by filling up a circle (pie chart). + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@interface MBRoundProgressView : UIView +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@interface MBRoundProgressView : NSView +{ + CGColorRef _cgColorFromNSColor; +} +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * Progress (0.0 to 1.0) + */ +@property (nonatomic, assign) float progress; + +/** + * Indicator progress color. + * Defaults to white [UIColor whiteColor] + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (nonatomic, MB_STRONG) UIColor *progressTintColor; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (nonatomic, MB_STRONG) NSColor *progressTintColor; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * Indicator background (non-progress) color. + * Defaults to translucent white (alpha 0.1) + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (nonatomic, MB_STRONG) UIColor *backgroundTintColor; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (nonatomic, MB_STRONG) NSColor *backgroundTintColor; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/* + * Display mode - NO = round or YES = annular. Defaults to round. + */ +@property (nonatomic, assign, getter = isAnnular) BOOL annular; + +@end + + +/** + * A flat bar progress view. + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@interface MBBarProgressView : UIView +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@interface MBBarProgressView : NSView +{ + CGColorRef _cgColorFromNSColor; +} +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * Progress (0.0 to 1.0) + */ +@property (nonatomic, assign) float progress; + +/** + * Bar border line color. + * Defaults to white [UIColor whiteColor]. + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (nonatomic, MB_STRONG) UIColor *lineColor; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (nonatomic, MB_STRONG) NSColor *lineColor; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * Bar background color. + * Defaults to clear [UIColor clearColor]; + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (nonatomic, MB_STRONG) UIColor *progressRemainingColor; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (nonatomic, MB_STRONG) NSColor *progressRemainingColor; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * Bar progress color. + * Defaults to white [UIColor whiteColor]. + */ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (nonatomic, MB_STRONG) UIColor *progressColor; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (nonatomic, MB_STRONG) NSColor *progressColor; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +/** + * OS X Helper method to convert NSColor value to CGColorRef value. + * + * @nscolor NSColur instance to convert. + * @return Converted NSColor to CGColorRef. + */ +#if !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (CGColorRef)NSColorToCGColor:(NSColor *)nscolor; +#endif // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +@end + +#if !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +/** + * A Spinner indefinite progress view modifying look of NSProgressIndicator. + */ +@interface MBSpinnerProgressView : NSProgressIndicator + +@end + +// +// YRKSpinningProgressIndicator.h +// +// Copyright 2009 Kelan Champagne. All rights reserved. +// +// Modified for ObjC-ARC compatibility by Wayne Fox 2014 + +@interface YRKSpinningProgressIndicator : NSView { + int _position; + int _numFins; +#if __has_feature(objc_arc) + NSMutableArray *_finColors; +#else + NSColor **_finColors; +#endif + + BOOL _isAnimating; + BOOL _isFadingOut; + NSTimer *_animationTimer; + NSThread *_animationThread; + + NSColor *_foreColor; + NSColor *_backColor; + BOOL _drawsBackground; + + BOOL _displayedWhenStopped; + BOOL _usesThreadedAnimation; + + // For determinate mode + BOOL _isIndeterminate; + double _currentValue; + double _maxValue; +} + +@property (nonatomic, retain) NSColor *color; +@property (nonatomic, retain) NSColor *backgroundColor; +@property (nonatomic, assign) BOOL drawsBackground; + +@property (nonatomic, assign, getter=isDisplayedWhenStopped) BOOL displayedWhenStopped; +@property (nonatomic, assign) BOOL usesThreadedAnimation; + +@property (nonatomic, assign, getter=isIndeterminate) BOOL indeterminate; +@property (nonatomic, assign) double doubleValue; +@property (nonatomic, assign) double maxValue; + +- (void)stopAnimation:(id)sender; +- (void)startAnimation:(id)sender; + +@end + +#endif // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) diff --git a/AppleParty/AppleParty/Vendors/MBProgressHUD-OSX/MBProgressHUD.m b/AppleParty/AppleParty/Vendors/MBProgressHUD-OSX/MBProgressHUD.m new file mode 100755 index 0000000..8318c08 --- /dev/null +++ b/AppleParty/AppleParty/Vendors/MBProgressHUD-OSX/MBProgressHUD.m @@ -0,0 +1,2110 @@ +// +// MBProgresHUD.m +// Created by vanelizarov © 2015 +// +// + +#import "MBProgressHUD.h" +#import + + +#if __has_feature(objc_arc) +#define MB_AUTORELEASE(exp) exp +#define MB_RELEASE(exp) exp +#define MB_RETAIN(exp) exp +#else +#define MB_AUTORELEASE(exp) [exp autorelease] +#define MB_RELEASE(exp) [exp release] +#define MB_RETAIN(exp) [exp retain] +#endif + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 +#define MBLabelAlignmentCenter NSTextAlignmentCenter +#else +#define MBLabelAlignmentCenter UITextAlignmentCenter +#endif + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 +#define MB_TEXTSIZE(text, font) [text length] > 0 ? [text \ +sizeWithAttributes:@{NSFontAttributeName:font}] : CGSizeZero; +#else +#define MB_TEXTSIZE(text, font) [text length] > 0 ? [text sizeWithFont:font] : CGSizeZero; +#endif + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 +#define MB_MULTILINE_TEXTSIZE(text, font, maxSize, mode) [text length] > 0 ? [text \ +boundingRectWithSize:maxSize options:(NSStringDrawingUsesLineFragmentOrigin) \ +attributes:@{NSFontAttributeName:font} context:nil].size : CGSizeZero; +#else +#define MB_MULTILINE_TEXTSIZE(text, font, maxSize, mode) [text length] > 0 ? [text \ +sizeWithFont:font constrainedToSize:maxSize lineBreakMode:mode] : CGSizeZero; +#endif + +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +#define MBLabelAlignmentCenter NSCenterTextAlignment +#define MB_TEXTSIZE(text, font) [text length] > 0 ? [text \ +sizeWithAttributes:@{NSFontAttributeName:font}] : CGSizeZero; +#define MB_MULTILINE_TEXTSIZE(text, font, maxSize, mode) [text length] > 0 ? [text \ +boundingRectWithSize:maxSize options:(NSStringDrawingUsesLineFragmentOrigin) \ +attributes:@{NSFontAttributeName:font} context:nil].size : CGSizeZero; + +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + + +static const CGFloat kPadding = 4.0f; +static const CGFloat kLabelFontSize = 16.0f; +static const CGFloat kDetailsLabelFontSize = 12.0f; + + +@interface MBProgressHUD () + +- (void)setupLabels; +- (void)registerForKVO; +- (void)unregisterFromKVO; +- (NSArray *)observableKeypaths; +- (void)registerForNotifications; +- (void)unregisterFromNotifications; +- (void)updateUIForKeypath:(NSString *)keyPath; +- (void)hideUsingAnimation:(BOOL)animated; +- (void)showUsingAnimation:(BOOL)animated; +- (void)done; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (void)singleTap:(UITapGestureRecognizer*)sender; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (void)mouseDown:(NSEvent *)theEvent; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (void)updateIndicators; +- (void)handleGraceTimer:(NSTimer *)theTimer; +- (void)handleMinShowTimer:(NSTimer *)theTimer; +- (void)setTransformForCurrentOrientation:(BOOL)animated; +- (void)cleanUp; +- (void)launchExecution; +- (void)deviceOrientationDidChange:(NSNotification *)notification; +- (void)hideDelayed:(NSNumber *)animated; + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (atomic, MB_STRONG) UIView *indicator; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (atomic, MB_STRONG) NSView *indicator; // YRKSpinningProgressIndicator +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +@property (atomic, MB_STRONG) NSTimer *graceTimer; +@property (atomic, MB_STRONG) NSTimer *minShowTimer; +@property (atomic, MB_STRONG) NSDate *showStarted; +@property (atomic, assign) CGSize size; + +@end + + +@implementation MBProgressHUD { + BOOL useAnimation; + SEL methodForExecution; + id targetForExecution; + id objectForExecution; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + UILabel *label; + UILabel *detailsLabel; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + NSText *label; + NSText *detailsLabel; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + BOOL isFinished; + CGAffineTransform rotationTransform; +} + +#pragma mark - Properties + +@synthesize animationType; +@synthesize delegate; +@synthesize opacity; +@synthesize color; +@synthesize labelFont; +@synthesize labelColor; +@synthesize detailsLabelFont; +@synthesize detailsLabelColor; +@synthesize indicator; +@synthesize xOffset; +@synthesize yOffset; +@synthesize minSize; +@synthesize square; +@synthesize spinsize; +@synthesize margin; +@synthesize dimBackground; +@synthesize dismissible; +@synthesize graceTime; +@synthesize minShowTime; +@synthesize graceTimer; +@synthesize minShowTimer; +@synthesize taskInProgress; +@synthesize removeFromSuperViewOnHide; +@synthesize customView; +@synthesize showStarted; +@synthesize mode; +@synthesize labelText; +@synthesize detailsLabelText; +@synthesize progress; +@synthesize size; +#if NS_BLOCKS_AVAILABLE +@synthesize completionBlock; +#endif + +#pragma mark - Class methods + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (MB_INSTANCETYPE)showHUDAddedTo:(UIView *)view animated:(BOOL)animated +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (MB_INSTANCETYPE)showHUDAddedTo:(NSView *)view animated:(BOOL)animated +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +{ + NSAssert(view, @"View must not be nil."); + MBProgressHUD *hud = [[self alloc] initWithView:view]; + [view addSubview:hud]; + [hud show:animated]; + return MB_AUTORELEASE(hud); +} + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (BOOL)hideHUDForView:(NSView *)view animated:(BOOL)animated +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +{ + NSAssert(view, @"View must not be nil."); + MBProgressHUD *hud = [self HUDForView:view]; + if (hud != nil) { + hud.removeFromSuperViewOnHide = YES; + [hud hide:animated]; + return YES; + } + return NO; +} + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (NSUInteger)hideAllHUDsForView:(UIView *)view animated:(BOOL)animated +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (NSUInteger)hideAllHUDsForView:(NSView *)view animated:(BOOL)animated +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +{ + NSAssert(view, @"View must not be nil."); + NSArray *huds = [MBProgressHUD allHUDsForView:view]; + for (MBProgressHUD *hud in huds) { + hud.removeFromSuperViewOnHide = YES; + [hud hide:animated]; + } + return [huds count]; +} + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (MB_INSTANCETYPE)HUDForView:(UIView *)view +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (MB_INSTANCETYPE)HUDForView:(NSView *)view +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +{ + NSAssert(view, @"View must not be nil."); + NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator]; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + for (UIView *subview in subviewsEnum) +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + for (NSView *subview in subviewsEnum) +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + { + if ([subview isKindOfClass:self]) { + return (MBProgressHUD *)subview; + } + } + return nil; +} + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (NSArray *)allHUDsForView:(UIView *)view +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) ++ (NSArray *)allHUDsForView:(NSView *)view +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +{ + NSAssert(view, @"View must not be nil."); + NSMutableArray *huds = [NSMutableArray array]; + NSArray *subviews = view.subviews; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + for (UIView *aView in subviews) +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + for (NSView *aView in subviews) +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + { + if ([aView isKindOfClass:self]) { + [huds addObject:aView]; + } + } + return [NSArray arrayWithArray:huds]; +} + +#if !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (CGColorRef)NSColorToCGColor:(NSColor *)nscolor +{ + NSInteger numberOfComponents = [nscolor numberOfComponents]; + CGFloat components[numberOfComponents]; + CGColorSpaceRef colorSpace = [[nscolor colorSpace] CGColorSpace]; + [nscolor getComponents:(CGFloat *)&components]; + if (_cgColorFromNSColor) { + CGColorRelease(_cgColorFromNSColor); + _cgColorFromNSColor = nil; + } + _cgColorFromNSColor = CGColorCreate(colorSpace, components); + return _cgColorFromNSColor; +} +#endif // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +#pragma mark - Lifecycle + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + // Set default values for properties + self.animationType = MBProgressHUDAnimationFade; + self.mode = MBProgressHUDModeIndeterminate; + self.labelText = nil; + self.detailsLabelText = nil; + self.opacity = 0.8f; + self.color = nil; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.labelFont = [UIFont boldSystemFontOfSize:kLabelFontSize]; + self.labelColor = [UIColor whiteColor]; + self.detailsLabelFont = [UIFont boldSystemFontOfSize:kDetailsLabelFontSize]; + self.detailsLabelColor = [UIColor whiteColor]; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.labelFont = [NSFont boldSystemFontOfSize:kLabelFontSize]; + self.labelColor = [NSColor whiteColor]; + self.detailsLabelFont = [NSFont boldSystemFontOfSize:kDetailsLabelFontSize]; + self.detailsLabelColor = [NSColor whiteColor]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.xOffset = 0.0f; + self.yOffset = 0.0f; + self.dimBackground = NO; + self.dismissible = NO; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.spinsize = 37.0f; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.spinsize = 60.0f; // Applicable to Mac OS X ONLY +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.margin = 20.0f; + self.cornerRadius = 10.0f; + self.graceTime = 0.0f; + self.minShowTime = 0.0f; + self.removeFromSuperViewOnHide = NO; + self.minSize = CGSizeZero; + self.square = NO; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin + | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + /* + typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) { + UIViewAutoresizingNone = 0, // 0 + UIViewAutoresizingFlexibleLeftMargin = 1 << 0, // 1 + UIViewAutoresizingFlexibleWidth = 1 << 1, // 2 + UIViewAutoresizingFlexibleRightMargin = 1 << 2, // 4 + UIViewAutoresizingFlexibleTopMargin = 1 << 3, // 8 + UIViewAutoresizingFlexibleHeight = 1 << 4, // 16 + UIViewAutoresizingFlexibleBottomMargin = 1 << 5 // 32 + }; + enum { + NSViewNotSizable = 0, + NSViewMinXMargin = 1, + NSViewWidthSizable = 2, + NSViewMaxXMargin = 4, + NSViewMinYMargin = 8, + NSViewHeightSizable = 16, + NSViewMaxYMargin = 32 + }; + enum { + NSViewAutoresizingNone = NSViewNotSizable, + NSViewAutoresizingFlexibleLeftMargin = NSViewMinXMargin, + NSViewAutoresizingFlexibleWidth = NSViewWidthSizable, + NSViewAutoresizingFlexibleRightMargin = NSViewMaxXMargin, + NSViewAutoresizingFlexibleTopMargin = NSViewMaxYMargin, + NSViewAutoresizingFlexibleHeight = NSViewHeightSizable, + NSViewAutoresizingFlexibleBottomMargin = NSViewMinYMargin + }; + */ + self.autoresizingMask = NSViewAutoresizingFlexibleTopMargin | NSViewAutoresizingFlexibleBottomMargin + | NSViewAutoresizingFlexibleLeftMargin | NSViewAutoresizingFlexibleRightMargin; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + + // Transparent background +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.opaque = NO; + self.backgroundColor = [UIColor clearColor]; + // Make it invisible for now + self.alpha = 0.0f; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.layer.opaque = NO; + self.layer.backgroundColor = [self NSColorToCGColor:[NSColor clearColor]]; + CGColorRelease(_cgColorFromNSColor), _cgColorFromNSColor = nil; + // Make it invisible for now + self.alphaValue = 0.0f; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + + taskInProgress = NO; + rotationTransform = CGAffineTransformIdentity; + + [self setupLabels]; + // [self updateIndicators]; + [self registerForKVO]; + [self registerForNotifications]; + } + return self; +} + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (id)initWithView:(UIView *)view +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (id)initWithView:(NSView *)view +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +{ + NSAssert(view, @"View must not be nil."); + CGRect bounds = view.frame; + bounds.origin.x = 0.0f; + bounds.origin.y = 0.0f; + return [self initWithFrame:bounds]; +} + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (id)initWithWindow:(UIWindow *)window +{ + return [self initWithView:(UIView *)window]; +} +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (id)initWithWindow:(NSWindow *)window +{ + return [self initWithView:(NSView *)window]; +} +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +- (void)dealloc +{ + [self unregisterFromNotifications]; + [self unregisterFromKVO]; +#if !__has_feature(objc_arc) + [color release]; + [indicator release]; + [label release]; + [detailsLabel release]; + [labelText release]; + [detailsLabelText release]; + [graceTimer release]; + [minShowTimer release]; + [showStarted release]; + [customView release]; + [labelFont release]; + [labelColor release]; + [detailsLabelFont release]; + [detailsLabelColor release]; +#if !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if (_cgColorFromNSColor) CGColorRelease(_cgColorFromNSColor); +#endif +#if NS_BLOCKS_AVAILABLE + [completionBlock release]; +#endif + + [super dealloc]; +#endif // !__has_feature(objc_arc) +} + +#pragma mark - Show & hide + +- (void)show:(BOOL)animated +{ + [self updateIndicators]; // allow self.spinsize to be effective + + useAnimation = animated; + // If the grace time is set postpone the HUD display + if (self.graceTime > 0.0) { + self.graceTimer = [NSTimer scheduledTimerWithTimeInterval:self.graceTime target:self + selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO]; + } + // ... otherwise show the HUD imediately + else { +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self setNeedsDisplay]; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self setNeedsDisplay:YES]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self showUsingAnimation:useAnimation]; + } +} + +- (void)hide:(BOOL)animated +{ + useAnimation = animated; + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + // If the minShow time is set, calculate how long the hud was shown, + // and pospone the hiding operation if necessary + if (self.minShowTime > 0.0 && showStarted) { + NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:showStarted]; + if (interv < self.minShowTime) { + self.minShowTimer = [NSTimer scheduledTimerWithTimeInterval:(self.minShowTime - interv) target:self + selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO]; + return; + } + } + // ... otherwise hide the HUD immediately + [self hideUsingAnimation:useAnimation]; +} + +- (void)hide:(BOOL)animated afterDelay:(NSTimeInterval)delay +{ + [self performSelector:@selector(hideDelayed:) withObject:[NSNumber numberWithBool:animated] afterDelay:delay]; +} + +- (void)hideDelayed:(NSNumber *)animated +{ + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + if (self.delegate) { + if ([self.delegate respondsToSelector:@selector(hudWasHiddenAfterDelay:)]) { + [self.delegate performSelector:@selector(hudWasHiddenAfterDelay:) withObject:self]; + } + } + + [self hide:[animated boolValue]]; +} + +- (BOOL)isFinished +{ + return isFinished; +} + +#pragma mark - Timer callbacks + +- (void)handleGraceTimer:(NSTimer *)theTimer +{ + // Show the HUD only if the task is still running + if (taskInProgress) { +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self setNeedsDisplay]; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self setNeedsDisplay:YES]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self showUsingAnimation:useAnimation]; + } +} + +- (void)handleMinShowTimer:(NSTimer *)theTimer +{ + [self hideUsingAnimation:useAnimation]; +} + +#pragma mark - View Hierrarchy + +- (void)didMoveToSuperview +{ + // We need to take care of rotation ourselfs if we're adding the HUD to a window +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if ([self.superview isKindOfClass:[UIWindow class]]) +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if ([self.superview isKindOfClass:[NSWindow class]]) +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + { + [self setTransformForCurrentOrientation:NO]; + } +} + +#pragma mark - Internal show & hide operations + +- (void)showUsingAnimation:(BOOL)animated +{ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if (animated && animationType == MBProgressHUDAnimationZoomIn) { + self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f)); + } else if (animated && animationType == MBProgressHUDAnimationZoomOut) { + self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f)); + } +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if (animated && animationType == MBProgressHUDAnimationZoomIn) { + self.layer.affineTransform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f)); + } else if (animated && animationType == MBProgressHUDAnimationZoomOut) { + self.layer.affineTransform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f)); + } +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.showStarted = [NSDate date]; + + // Fade in +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if (animated) { + [UIView beginAnimations:nil context:NULL]; + [UIView setAnimationDuration:0.30]; + self.alpha = 1.0f; + if (animationType == MBProgressHUDAnimationZoomIn || animationType == MBProgressHUDAnimationZoomOut) { + self.transform = rotationTransform; + } + [UIView commitAnimations]; + } + else { + self.alpha = 1.0f; + } +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.hidden = NO; + if (animated) { + [NSAnimationContext beginGrouping]; + [[NSAnimationContext currentContext] setDuration:0.30]; + [[self animator] setAlphaValue:1.0f]; + if (animationType == MBProgressHUDAnimationZoomIn || animationType == MBProgressHUDAnimationZoomOut) { + [(CALayer *)[self animator] setAffineTransform:rotationTransform]; + } + [NSAnimationContext endGrouping]; + } + else { + self.alphaValue = 1.0f; + } +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +} + +- (void)hideUsingAnimation:(BOOL)animated +{ + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + // Fade out +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if (animated && showStarted) { + [UIView beginAnimations:nil context:NULL]; + [UIView setAnimationDuration:0.30]; + [UIView setAnimationDelegate:self]; + [UIView setAnimationDidStopSelector:@selector(animationFinished:finished:context:)]; + // 0.02 prevents the hud from passing through touches during the animation the hud will get completely hidden + // in the done method + if (animationType == MBProgressHUDAnimationZoomIn) { + self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f)); + } else if (animationType == MBProgressHUDAnimationZoomOut) { + self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f)); + } + + self.alpha = 0.02f; + [UIView commitAnimations]; + } + else { + self.alpha = 0.0f; + [self done]; + } +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if (animated && showStarted) { + [NSAnimationContext beginGrouping]; + [[NSAnimationContext currentContext] setDuration:0.30]; + [[NSAnimationContext currentContext] setCompletionHandler:^{ + [self done]; + }]; + [(NSView *)[self animator] setAlphaValue:0.2f]; + if (animationType == MBProgressHUDAnimationZoomIn) { + [(CALayer *)[self animator] setAffineTransform:CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f))]; + } else if (animationType == MBProgressHUDAnimationZoomOut) { + [(CALayer *)[self animator] setAffineTransform:CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f))]; + } + [NSAnimationContext endGrouping]; + } + else { + self.alphaValue = 0.0f; + [self done]; + } +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.showStarted = nil; +} + +- (void)animationFinished:(NSString *)animationID finished:(BOOL)finished context:(void*)context +{ + [self done]; +} + +- (void)done +{ + isFinished = YES; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.alpha = 0.0f; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.alphaValue = 0.0f; + // self.acceptsTouchEvents = NO; + self.hidden = YES; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if (self.removeFromSuperViewOnHide) { + [self removeFromSuperview]; + } +#if NS_BLOCKS_AVAILABLE + if (self.completionBlock) { + self.completionBlock(); + self.completionBlock = NULL; + } +#endif + + if ([self.delegate class]) { + if ([self.delegate respondsToSelector:@selector(hudWasHidden:)]) { + [self.delegate performSelector:@selector(hudWasHidden:) withObject:self]; + } + } +} + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (void)singleTap:(UITapGestureRecognizer*)sender +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (void)mouseDown:(NSEvent *)theEvent +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +{ +#if !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if (isFinished) { + [super mouseDown:theEvent]; + return; + } +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if (self.dismissible) { + [self performSelectorOnMainThread:@selector(cleanUp) withObject:nil waitUntilDone:YES]; + if (self.delegate) { + if ([self.delegate respondsToSelector:@selector(hudWasTapped:)]) { + [self.delegate performSelector:@selector(hudWasTapped:) withObject:self]; + } + } + } +} + +#pragma mark - Threading + +- (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated +{ + methodForExecution = method; + targetForExecution = MB_RETAIN(target); + objectForExecution = MB_RETAIN(object); + // Launch execution in new thread + self.taskInProgress = YES; + [NSThread detachNewThreadSelector:@selector(launchExecution) toTarget:self withObject:nil]; + // Show HUD view + [self show:animated]; +} + +#if NS_BLOCKS_AVAILABLE + +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block +{ + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + [self showAnimated:animated whileExecutingBlock:block onQueue:queue completionBlock:NULL]; +} + +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block completionBlock:(void (^)())completion +{ + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + [self showAnimated:animated whileExecutingBlock:block onQueue:queue completionBlock:completion]; +} + +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue +{ + [self showAnimated:animated whileExecutingBlock:block onQueue:queue completionBlock:NULL]; +} + +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue + completionBlock:(MBProgressHUDCompletionBlock)completion +{ + self.taskInProgress = YES; + self.completionBlock = completion; + dispatch_async(queue, ^(void) { + block(); + dispatch_async(dispatch_get_main_queue(), ^(void) { + [self cleanUp]; + }); + }); + [self show:animated]; +} + +#endif + +- (void)launchExecution +{ + @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + // Start executing the requested task + [targetForExecution performSelector:methodForExecution withObject:objectForExecution]; +#pragma clang diagnostic pop + // Task completed, update view in main thread (note: view operations should + // be done only in the main thread) + [self performSelectorOnMainThread:@selector(cleanUp) withObject:nil waitUntilDone:NO]; + } +} + +- (void)cleanUp +{ + taskInProgress = NO; +#if !__has_feature(objc_arc) + [targetForExecution release]; + [objectForExecution release]; +#endif + targetForExecution = nil; + objectForExecution = nil; + + [self hide:useAnimation]; +} + +#pragma mark - UI + +- (void)setupLabels +{ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)]; + label = [[UILabel alloc] initWithFrame:self.bounds]; + label.adjustsFontSizeToFitWidth = NO; + label.textAlignment = MBLabelAlignmentCenter; + label.opaque = NO; + label.backgroundColor = [UIColor clearColor]; + label.textColor = self.labelColor; + label.font = self.labelFont; + label.text = self.labelText; + [label addGestureRecognizer:singleTapGesture]; +#else // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + label = [[NSText alloc] initWithFrame:self.bounds]; + // label.adjustsFontSizeToFitWidth = NO; + label.editable = NO; + // label.bezeled = NO; // if NSTextView + label.alignment = MBLabelAlignmentCenter; + label.layer.opaque = NO; + label.backgroundColor = [NSColor clearColor]; + label.textColor = self.labelColor; + label.font = self.labelFont; + if (self.labelText) label.string = self.labelText; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self addSubview:label]; + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + detailsLabel = [[UILabel alloc] initWithFrame:self.bounds]; + detailsLabel.font = self.detailsLabelFont; + detailsLabel.adjustsFontSizeToFitWidth = NO; + detailsLabel.textAlignment = MBLabelAlignmentCenter; + detailsLabel.opaque = NO; + detailsLabel.backgroundColor = [UIColor clearColor]; + detailsLabel.textColor = self.detailsLabelColor; + detailsLabel.numberOfLines = 0; + detailsLabel.font = self.detailsLabelFont; + detailsLabel.text = self.detailsLabelText; + [detailsLabel addGestureRecognizer:singleTapGesture]; +#else // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + detailsLabel = [[NSText alloc] initWithFrame:self.bounds]; + detailsLabel.font = self.detailsLabelFont; + // detailsLabel.adjustsFontSizeToFitWidth = NO; + detailsLabel.editable = NO; + // detailsLabel.bezeled = NO; // if NSTextView + detailsLabel.alignment = MBLabelAlignmentCenter; + detailsLabel.layer.opaque = NO; + detailsLabel.backgroundColor = [NSColor clearColor]; + detailsLabel.textColor = self.detailsLabelColor; + // detailsLabel.numberOfLines = 0; + detailsLabel.font = self.detailsLabelFont; + if (self.detailsLabelText) detailsLabel.string = self.detailsLabelText; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self addSubview:detailsLabel]; + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self addGestureRecognizer:singleTapGesture]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +} + +- (void)updateIndicators +{ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + BOOL isActivityIndicator = [indicator isKindOfClass:[UIActivityIndicatorView class]]; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + BOOL isActivityIndicator = [indicator isKindOfClass:[YRKSpinningProgressIndicator class]]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + BOOL isRoundIndicator = [indicator isKindOfClass:[MBRoundProgressView class]]; + + if (mode == MBProgressHUDModeIndeterminate && !isActivityIndicator) { + // Update to indeterminate indicator + [indicator removeFromSuperview]; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.indicator = MB_AUTORELEASE([[UIActivityIndicatorView alloc] + initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]); + [(UIActivityIndicatorView *)indicator startAnimating]; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.indicator = MB_AUTORELEASE([[YRKSpinningProgressIndicator alloc] initWithFrame:NSMakeRect(20, 20, self.spinsize, self.spinsize)]); + // [(YRKSpinningProgressIndicator *)self.indicator setStyle:NSProgressIndicatorSpinningStyle]; + [(YRKSpinningProgressIndicator *)self.indicator setColor:[NSColor whiteColor]]; + [(YRKSpinningProgressIndicator *)self.indicator setUsesThreadedAnimation:NO]; + [(YRKSpinningProgressIndicator *)self.indicator startAnimation:self]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self addSubview:indicator]; + } + else if (mode == MBProgressHUDModeDeterminateHorizontalBar) { + // Update to bar determinate indicator + [indicator removeFromSuperview]; + self.indicator = MB_AUTORELEASE([[MBBarProgressView alloc] init]); + [self addSubview:indicator]; + } + else if (mode == MBProgressHUDModeDeterminate || mode == MBProgressHUDModeAnnularDeterminate) { + if (!isRoundIndicator) { + // Update to determinante indicator + [indicator removeFromSuperview]; + // self.indicator = [MB_AUTORELEASE([[MBRoundProgressView alloc] init]); + self.indicator = MB_AUTORELEASE([[MBRoundProgressView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, self.spinsize, self.spinsize)]); + [self addSubview:indicator]; + } + if (mode == MBProgressHUDModeAnnularDeterminate) { + [(MBRoundProgressView *)indicator setAnnular:YES]; + } + } + else if (mode == MBProgressHUDModeCustomView && customView != indicator) { + // Update custom view indicator + [indicator removeFromSuperview]; + self.indicator = customView; + [self addSubview:indicator]; + } else if (mode == MBProgressHUDModeText) { + [indicator removeFromSuperview]; + self.indicator = nil; + } +} + +#pragma mark - Layout + +- (void)layoutSubviews +{ + + // Entirely cover the parent view +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + UIView *parent = self.superview; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + NSView *parent = self.superview; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if (parent) { + self.frame = parent.bounds; + } + CGRect bounds = self.bounds; + + // Determine the total widt and height needed + CGFloat maxWidth = bounds.size.width - 4 * margin; + CGSize totalSize = CGSizeZero; + + CGRect indicatorF = indicator.bounds; + indicatorF.size.width = MIN(indicatorF.size.width, maxWidth); + totalSize.width = MAX(totalSize.width, indicatorF.size.width); + totalSize.height += indicatorF.size.height; + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGSize labelSize = MB_TEXTSIZE(label.text, label.font); +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGSize labelSize = MB_TEXTSIZE(label.string, label.font); + if (labelSize.width > 0.0f) labelSize.width += 10.0f; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + labelSize.width = MIN(labelSize.width, maxWidth); + totalSize.width = MAX(totalSize.width, labelSize.width); + totalSize.height += labelSize.height; + if (labelSize.height > 0.0f && indicatorF.size.height > 0.0f) { + totalSize.height += kPadding; + } + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGFloat remainingHeight = bounds.size.height - totalSize.height - kPadding - 4 * margin; + CGSize maxSize = CGSizeMake(maxWidth, remainingHeight); + CGSize detailsLabelSize = MB_MULTILINE_TEXTSIZE(detailsLabel.text, detailsLabel.font, maxSize, detailsLabel.lineBreakMode); +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGSize detailsLabelSize = MB_TEXTSIZE(detailsLabel.string, detailsLabel.font); + if (detailsLabelSize.width > 0.0f) detailsLabelSize.width += 10.0f; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + detailsLabelSize.width = MIN(detailsLabelSize.width, maxWidth); + totalSize.width = MAX(totalSize.width, detailsLabelSize.width); + totalSize.height += detailsLabelSize.height; + if (detailsLabelSize.height > 0.0f && (indicatorF.size.height > 0.0f || labelSize.height > 0.0f)) { + totalSize.height += kPadding; + } + + totalSize.width += 2 * margin; + totalSize.height += 2 * margin; + + // Position elements +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGFloat yPos = round(((bounds.size.height - totalSize.height) / 2)) + margin + yOffset; +#else + // in OS X yAxis is inverted! So Top Down Must build from bottom up... + CGFloat yPos = round(((bounds.size.height - totalSize.height) / 2)) + margin - yOffset; + if (labelSize.height > 0.0f && indicatorF.size.height > 0.0f) { + yPos += kPadding + labelSize.height; + } + if (detailsLabelSize.height > 0.0f && (indicatorF.size.height > 0.0f || labelSize.height > 0.0f)) { + yPos += kPadding + detailsLabelSize.height; + } +#endif + CGFloat xPos = xOffset; + indicatorF.origin.y = yPos; + indicatorF.origin.x = round((bounds.size.width - indicatorF.size.width) / 2) + xPos; + indicator.frame = indicatorF; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + yPos += indicatorF.size.height; + + if (labelSize.height > 0.0f && indicatorF.size.height > 0.0f) { + yPos += kPadding; + } +#else + // in OS X yAxis is inverted! So Top Down Must build from bottom up... + if (labelSize.height > 0.0f && indicatorF.size.height > 0.0f) { + yPos -= (kPadding + labelSize.height); + } +#endif + CGRect labelF; + labelF.origin.y = yPos; + labelF.origin.x = round((bounds.size.width - labelSize.width) / 2) + xPos; + labelF.size = labelSize; + label.frame = labelF; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + yPos += labelF.size.height; + + if (detailsLabelSize.height > 0.0f && (indicatorF.size.height > 0.0f || labelSize.height > 0.0f)) { + yPos += kPadding; + } +#else + // in OS X yAxis is inverted! So Top Down Must build from bottom up... + if (detailsLabelSize.height > 0.0f && (indicatorF.size.height > 0.0f || labelSize.height > 0.0f)) { + yPos -= (kPadding + detailsLabelSize.height); + } +#endif + CGRect detailsLabelF; + detailsLabelF.origin.y = yPos; + detailsLabelF.origin.x = round((bounds.size.width - detailsLabelSize.width) / 2) + xPos; + detailsLabelF.size = detailsLabelSize; + detailsLabel.frame = detailsLabelF; + + // Enforce minsize and quare rules + if (square) { + CGFloat max = MAX(totalSize.width, totalSize.height); + if (max <= bounds.size.width - 2 * margin) { + totalSize.width = max; + } + if (max <= bounds.size.height - 2 * margin) { + totalSize.height = max; + } + } + if (totalSize.width < minSize.width) { + totalSize.width = minSize.width; + } + if (totalSize.height < minSize.height) { + totalSize.height = minSize.height; + } + + self.size = totalSize; +} + +#pragma mark - BG Drawing + +- (void)drawRect:(CGRect)rect +{ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGContextRef context = UIGraphicsGetCurrentContext(); +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self layoutSubviews]; + //CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + UIGraphicsPushContext(context); + context = UIGraphicsGetCurrentContext(); +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [NSGraphicsContext saveGraphicsState]; + CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + + if (self.dimBackground) { + //Gradient colours + size_t gradLocationsNum = 2; + CGFloat gradLocations[2] = {0.0f, 1.0f}; + CGFloat gradColors[8] = {0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.75f}; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradColors, gradLocations, gradLocationsNum); + CGColorSpaceRelease(colorSpace); + //Gradient center + CGPoint gradCenter= CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2); + //Gradient radius + float gradRadius = MIN(self.bounds.size.width , self.bounds.size.height) ; + //Gradient draw + CGContextDrawRadialGradient (context, gradient, gradCenter, + 0, gradCenter, gradRadius, + kCGGradientDrawsAfterEndLocation); + CGGradientRelease(gradient); + } + + // Set background rect color + if (self.color) { + if (context > 0) { +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGContextSetFillColorWithColor(context, self.color.CGColor); +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGContextSetFillColorWithColor(context, [self NSColorToCGColor:self.color]); + CGColorRelease(_cgColorFromNSColor), _cgColorFromNSColor = nil; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + } + } else { + if (context > 0) { + CGContextSetGrayFillColor(context, 0.0f, self.opacity); + } + } + + + // Center HUD + CGRect allRect = self.bounds; + + // Draw rounded HUD backgroud rect +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGRect boxRect = CGRectMake(round((allRect.size.width - size.width) / 2) + self.xOffset, + round((allRect.size.height - size.height) / 2) + self.yOffset, size.width, size.height); +#else + CGRect boxRect = CGRectMake(round((allRect.size.width - size.width) / 2) + self.xOffset, + round((allRect.size.height - size.height) / 2) - self.yOffset, size.width, size.height); +#endif + + float radius = self.cornerRadius; + CGContextBeginPath(context); + CGContextMoveToPoint(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect)); + CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMinY(boxRect) + radius, radius, 3 * (float)M_PI / 2, 0, 0); + CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMaxY(boxRect) - radius, radius, 0, (float)M_PI / 2, 0); + CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMaxY(boxRect) - radius, radius, (float)M_PI / 2, (float)M_PI, 0); + CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect) + radius, radius, (float)M_PI, 3 * (float)M_PI / 2, 0); + CGContextClosePath(context); + CGContextFillPath(context); + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + UIGraphicsPopContext(); +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [NSGraphicsContext restoreGraphicsState]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +} + +#pragma mark - KVO + +- (void)registerForKVO +{ + for (NSString *keyPath in [self observableKeypaths]) { + [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; + } +} + +- (void)unregisterFromKVO +{ + for (NSString *keyPath in [self observableKeypaths]) { + [self removeObserver:self forKeyPath:keyPath]; + } +} + +- (NSArray *)observableKeypaths +{ + return [NSArray arrayWithObjects:@"mode", @"customView", @"labelText", @"labelFont", @"labelColor", + @"detailsLabelText", @"detailsLabelFont", @"detailsLabelColor", @"progress", nil]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (![NSThread isMainThread]) { + [self performSelectorOnMainThread:@selector(updateUIForKeypath:) withObject:keyPath waitUntilDone:NO]; + } else { + [self updateUIForKeypath:keyPath]; + } +} + +- (void)updateUIForKeypath:(NSString *)keyPath +{ + if ([keyPath isEqualToString:@"mode"] || [keyPath isEqualToString:@"customView"]) { + [self updateIndicators]; + } else if ([keyPath isEqualToString:@"labelText"]) { +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + label.text = self.labelText; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + label.string = self.labelText; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + } else if ([keyPath isEqualToString:@"labelFont"]) { + label.font = self.labelFont; + } else if ([keyPath isEqualToString:@"labelColor"]) { + label.textColor = self.labelColor; + } else if ([keyPath isEqualToString:@"detailsLabelText"]) { +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + detailsLabel.text = self.detailsLabelText; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + detailsLabel.string = self.detailsLabelText; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + } else if ([keyPath isEqualToString:@"detailsLabelFont"]) { + detailsLabel.font = self.detailsLabelFont; + } else if ([keyPath isEqualToString:@"detailsLabelColor"]) { + detailsLabel.textColor = self.detailsLabelColor; + } else if ([keyPath isEqualToString:@"progress"]) { + if ([indicator respondsToSelector:@selector(setProgress:)]) { + [(id)indicator setValue:@(progress) forKey:@"progress"]; + } + return; + } +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self setNeedsLayout]; + [self setNeedsDisplay]; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self setNeedsLayout:YES]; + [self setNeedsDisplay:YES]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +} + +#pragma mark - Notifications + +- (void)registerForNotifications +{ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc addObserver:self selector:@selector(deviceOrientationDidChange:) + name:UIDeviceOrientationDidChangeNotification object:nil]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +} + +- (void)unregisterFromNotifications +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)deviceOrientationDidChange:(NSNotification *)notification +{ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + UIView *superview = self.superview; + if (!superview) { + return; + } else if ([superview isKindOfClass:[UIWindow class]]) { + [self setTransformForCurrentOrientation:YES]; + } else { + self.frame = self.superview.bounds; + [self setNeedsDisplay]; + } +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +} + +- (void)setTransformForCurrentOrientation:(BOOL)animated +{ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + // Stay in sync with the superview + if (self.superview) { + self.bounds = self.superview.bounds; + [self setNeedsDisplay]; + } + + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + CGFloat radians = 0; + if (UIInterfaceOrientationIsLandscape(orientation)) { + if (orientation == UIInterfaceOrientationLandscapeLeft) { radians = -(CGFloat)M_PI_2; } + else { radians = (CGFloat)M_PI_2; } + // Window coordinates differ! + self.bounds = CGRectMake(0, 0, self.bounds.size.height, self.bounds.size.width); + } else { + if (orientation == UIInterfaceOrientationPortraitUpsideDown) { radians = (CGFloat)M_PI; } + else { radians = 0; } + } + rotationTransform = CGAffineTransformMakeRotation(radians); + + if (animated) { + [UIView beginAnimations:nil context:nil]; + } + [self setTransform:rotationTransform]; + if (animated) { + [UIView commitAnimations]; + } +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +} + +@end + + +@implementation MBRoundProgressView + +#pragma mark - Lifecycle + +- (id)init +{ + return [self initWithFrame:CGRectMake(0.0f, 0.0f, 37.0f, 37.0f)]; +} + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.backgroundColor = [UIColor clearColor]; + self.opaque = NO; + _progress = 0.0f; + _annular = NO; + _progressTintColor = [[UIColor alloc] initWithWhite:1.0f alpha:1.0f]; + _backgroundTintColor = [[UIColor alloc] initWithWhite:1.0f alpha:0.1f]; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + self.layer.backgroundColor = [self NSColorToCGColor:[NSColor clearColor]]; + CGColorRelease(_cgColorFromNSColor), _cgColorFromNSColor = nil; + self.layer.opaque = NO; + _progress = 0.0f; + _annular = NO; + _progressTintColor = [NSColor colorWithDeviceWhite:1.0f alpha:1.0f]; + _backgroundTintColor = [NSColor colorWithDeviceWhite:1.0f alpha:0.1f]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self registerForKVO]; + } + return self; +} + +- (void)dealloc +{ + [self unregisterFromKVO]; +#if !__has_feature(objc_arc) + [_progressTintColor release]; + [_backgroundTintColor release]; +#if !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if (_cgColorFromNSColor) CGColorRelease(_cgColorFromNSColor); +#endif + + [super dealloc]; +#endif +} + +#if !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (CGColorRef)NSColorToCGColor:(NSColor *)nscolor +{ + NSInteger numberOfComponents = [nscolor numberOfComponents]; + CGFloat components[numberOfComponents]; + CGColorSpaceRef colorSpace = [[nscolor colorSpace] CGColorSpace]; + [nscolor getComponents:(CGFloat *)&components]; + if (_cgColorFromNSColor) { + CGColorRelease(_cgColorFromNSColor); + _cgColorFromNSColor = nil; + } + _cgColorFromNSColor = CGColorCreate(colorSpace, components); + return _cgColorFromNSColor; +} +#endif // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +#pragma mark - Drawing + +- (void)drawRect:(CGRect)rect +{ + + CGRect allRect = self.bounds; + CGRect circleRect = CGRectInset(allRect, 2.0f, 2.0f); + +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGContextRef context = UIGraphicsGetCurrentContext(); +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + + if (_annular) { + // Draw background + CGFloat lineWidth = 5.0f; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + UIBezierPath *processBackgroundPath = [UIBezierPath bezierPath]; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + NSBezierPath *processBackgroundPath = [NSBezierPath bezierPath]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + processBackgroundPath.lineWidth = lineWidth; + processBackgroundPath.lineCapStyle = kCGLineCapRound; + CGPoint center = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2); + CGFloat radius = (self.bounds.size.width - lineWidth)/2; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGFloat startAngle = - ((float)M_PI / 2); // 90 degrees + CGFloat endAngle = (2 * (float)M_PI) + startAngle; + [processBackgroundPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES]; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGFloat startAngle = ((float)M_PI / 2); // 90 degrees + CGFloat endAngle = startAngle - (2 * (float)M_PI); + [processBackgroundPath appendBezierPathWithArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:NO]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [_backgroundTintColor set]; + [processBackgroundPath stroke]; + // Draw progress +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + UIBezierPath *processPath = [UIBezierPath bezierPath]; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + NSBezierPath *processPath = [NSBezierPath bezierPath]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + processPath.lineCapStyle = kCGLineCapRound; + processPath.lineWidth = lineWidth; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + endAngle = (self.progress * 2 * (float)M_PI) + startAngle; + [processPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES]; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + endAngle = startAngle - (self.progress * 2 * (float)M_PI); + [processPath appendBezierPathWithArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [_progressTintColor set]; + [processPath stroke]; + } else { + // Draw background + [_progressTintColor setStroke]; + [_backgroundTintColor setFill]; + CGContextSetLineWidth(context, 2.0f); + CGContextFillEllipseInRect(context, circleRect); + CGContextStrokeEllipseInRect(context, circleRect); + // Draw progress + CGPoint center = CGPointMake(allRect.size.width / 2, allRect.size.height / 2); + CGFloat radius = (allRect.size.width - 4) / 2; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGFloat startAngle = - ((float)M_PI / 2); // 90 degrees + CGFloat endAngle = (self.progress * 2 * (float)M_PI) + startAngle; +#else + CGFloat startAngle = ((float)M_PI / 2); // 90 degrees + CGFloat endAngle = startAngle - (self.progress * 2 * (float)M_PI); +#endif + CGContextSetRGBFillColor(context, 1.0f, 1.0f, 1.0f, 1.0f); // white + CGContextMoveToPoint(context, center.x, center.y); +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0); +#else + CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 1); +#endif + CGContextClosePath(context); + CGContextFillPath(context); + } +} + +#pragma mark - KVO + +- (void)registerForKVO +{ + for (NSString *keyPath in [self observableKeypaths]) { + [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; + } +} + +- (void)unregisterFromKVO +{ + for (NSString *keyPath in [self observableKeypaths]) { + [self removeObserver:self forKeyPath:keyPath]; + } +} + +- (NSArray *)observableKeypaths +{ + return [NSArray arrayWithObjects:@"progressTintColor", @"backgroundTintColor", @"progress", @"annular", nil]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self setNeedsDisplay]; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self setNeedsDisplay:YES]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +} + +@end + + +@implementation MBBarProgressView + +#pragma mark - Lifecycle + +- (id)init +{ + return [self initWithFrame:CGRectMake(0.0f, 0.0f, 120.0f, 20.0f)]; +} + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + _progress = 0.0f; +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + _lineColor = [UIColor whiteColor]; + _progressColor = [UIColor whiteColor]; + _progressRemainingColor = [UIColor clearColor]; + self.backgroundColor = [UIColor clearColor]; + self.opaque = NO; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + _lineColor = [NSColor whiteColor]; + _progressColor = [NSColor whiteColor]; + _progressRemainingColor = [NSColor clearColor]; + self.layer.backgroundColor = [self NSColorToCGColor:[NSColor clearColor]]; + CGColorRelease(_cgColorFromNSColor), _cgColorFromNSColor = nil; + self.layer.opaque = NO; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self registerForKVO]; + } + return self; +} + +- (void)dealloc +{ + [self unregisterFromKVO]; +#if !__has_feature(objc_arc) + [_lineColor release]; + [_progressColor release]; + [_progressRemainingColor release]; +#if !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + if (_cgColorFromNSColor) CGColorRelease(_cgColorFromNSColor); +#endif + + [super dealloc]; +#endif +} + +#if !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +- (CGColorRef)NSColorToCGColor:(NSColor *)nscolor +{ + NSInteger numberOfComponents = [nscolor numberOfComponents]; + CGFloat components[numberOfComponents]; + CGColorSpaceRef colorSpace = [[nscolor colorSpace] CGColorSpace]; + [nscolor getComponents:(CGFloat *)&components]; + if (_cgColorFromNSColor) { + CGColorRelease(_cgColorFromNSColor); + _cgColorFromNSColor = nil; + } + _cgColorFromNSColor = CGColorCreate(colorSpace, components); + return _cgColorFromNSColor; +} +#endif // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +#pragma mark - Drawing + +- (void)drawRect:(CGRect)rect +{ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGContextRef context = UIGraphicsGetCurrentContext(); + + // setup properties + CGContextSetLineWidth(context, 2); + CGContextSetStrokeColorWithColor(context,[_lineColor CGColor]); + CGContextSetFillColorWithColor(context, [_progressRemainingColor CGColor]); +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort]; + + // setup properties + CGContextSetLineWidth(context, 2); + CGContextSetStrokeColorWithColor(context,[self NSColorToCGColor:_lineColor]); + CGColorRelease(_cgColorFromNSColor), _cgColorFromNSColor = nil; + CGContextSetFillColorWithColor(context, [self NSColorToCGColor:_progressRemainingColor]); + CGColorRelease(_cgColorFromNSColor), _cgColorFromNSColor = nil; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + + // draw line border + float radius = (rect.size.height / 2) - 2; + CGContextMoveToPoint(context, 2, rect.size.height/2); + CGContextAddArcToPoint(context, 2, 2, radius + 2, 2, radius); + CGContextAddLineToPoint(context, rect.size.width - radius - 2, 2); + CGContextAddArcToPoint(context, rect.size.width - 2, 2, rect.size.width - 2, rect.size.height / 2, radius); + CGContextAddArcToPoint(context, rect.size.width - 2, rect.size.height - 2, rect.size.width - radius - 2, rect.size.height - 2, radius); + CGContextAddLineToPoint(context, radius + 2, rect.size.height - 2); + CGContextAddArcToPoint(context, 2, rect.size.height - 2, 2, rect.size.height/2, radius); + CGContextFillPath(context); + + // draw progress background + CGContextMoveToPoint(context, 2, rect.size.height/2); + CGContextAddArcToPoint(context, 2, 2, radius + 2, 2, radius); + CGContextAddLineToPoint(context, rect.size.width - radius - 2, 2); + CGContextAddArcToPoint(context, rect.size.width - 2, 2, rect.size.width - 2, rect.size.height / 2, radius); + CGContextAddArcToPoint(context, rect.size.width - 2, rect.size.height - 2, rect.size.width - radius - 2, rect.size.height - 2, radius); + CGContextAddLineToPoint(context, radius + 2, rect.size.height - 2); + CGContextAddArcToPoint(context, 2, rect.size.height - 2, 2, rect.size.height/2, radius); + CGContextStrokePath(context); + + // setup to draw progress color +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGContextSetFillColorWithColor(context, [_progressColor CGColor]); +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + CGContextSetFillColorWithColor(context, [self NSColorToCGColor:_progressColor]); + CGColorRelease(_cgColorFromNSColor), _cgColorFromNSColor = nil; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + radius = radius - 2; + float amount = self.progress * rect.size.width; + + // if progress is in the middle area + if (amount >= radius + 4 && amount <= (rect.size.width - radius - 4)) { + // top + CGContextMoveToPoint(context, 4, rect.size.height/2); + CGContextAddArcToPoint(context, 4, 4, radius + 4, 4, radius); + CGContextAddLineToPoint(context, amount, 4); + CGContextAddLineToPoint(context, amount, radius + 4); + + // bottom + CGContextMoveToPoint(context, 4, rect.size.height/2); + CGContextAddArcToPoint(context, 4, rect.size.height - 4, radius + 4, rect.size.height - 4, radius); + CGContextAddLineToPoint(context, amount, rect.size.height - 4); + CGContextAddLineToPoint(context, amount, radius + 4); + + CGContextFillPath(context); + } + + // progress is in the right arc + else if (amount > radius + 4) { + float x = amount - (rect.size.width - radius - 4); + + // top + CGContextMoveToPoint(context, 4, rect.size.height/2); + CGContextAddArcToPoint(context, 4, 4, radius + 4, 4, radius); + CGContextAddLineToPoint(context, rect.size.width - radius - 4, 4); + float angle = -acos(x/radius); + if (isnan(angle)) angle = 0; + CGContextAddArc(context, rect.size.width - radius - 4, rect.size.height/2, radius, M_PI, angle, 0); + CGContextAddLineToPoint(context, amount, rect.size.height/2); + + // bottom + CGContextMoveToPoint(context, 4, rect.size.height/2); + CGContextAddArcToPoint(context, 4, rect.size.height - 4, radius + 4, rect.size.height - 4, radius); + CGContextAddLineToPoint(context, rect.size.width - radius - 4, rect.size.height - 4); + angle = acos(x/radius); + if (isnan(angle)) angle = 0; + CGContextAddArc(context, rect.size.width - radius - 4, rect.size.height/2, radius, -M_PI, angle, 1); + CGContextAddLineToPoint(context, amount, rect.size.height/2); + + CGContextFillPath(context); + } + + // progress is in the left arc + else if (amount < radius + 4 && amount > 0) { + // top + CGContextMoveToPoint(context, 4, rect.size.height/2); + CGContextAddArcToPoint(context, 4, 4, radius + 4, 4, radius); + CGContextAddLineToPoint(context, radius + 4, rect.size.height/2); + + // bottom + CGContextMoveToPoint(context, 4, rect.size.height/2); + CGContextAddArcToPoint(context, 4, rect.size.height - 4, radius + 4, rect.size.height - 4, radius); + CGContextAddLineToPoint(context, radius + 4, rect.size.height/2); + + CGContextFillPath(context); + } +} + +#pragma mark - KVO + +- (void)registerForKVO +{ + for (NSString *keyPath in [self observableKeypaths]) { + [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; + } +} + +- (void)unregisterFromKVO +{ + for (NSString *keyPath in [self observableKeypaths]) { + [self removeObserver:self forKeyPath:keyPath]; + } +} + +- (NSArray *)observableKeypaths +{ + return [NSArray arrayWithObjects:@"lineColor", @"progressRemainingColor", @"progressColor", @"progress", nil]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ +#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self setNeedsDisplay]; +#else // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + [self setNeedsDisplay:YES]; +#endif // (TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) +} + +@end + +#if !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) + +@implementation MBSpinnerProgressView + +// Create subclass of NSProgressIndicator inside method do like this + +#pragma mark - Drawing + +- (void)drawRect:(NSRect)dirtyRect +{ + // Drawing code here. + [self setControlTint:NSGraphiteControlTint]; + CGContextSetBlendMode((CGContextRef)[[NSGraphicsContext currentContext] graphicsPort], kCGBlendModeSoftLight); + [[[NSColor whiteColor] colorWithAlphaComponent:1] set]; + [NSBezierPath fillRect:dirtyRect]; + [super drawRect:dirtyRect]; +} + +@end + +// +// YRKSpinningProgressIndicator.m +// +// Copyright 2009 Kelan Champagne. All rights reserved. +// +// Modified for ObjC-ARC compatibility by Wayne Fox 2014 + +// Some constants to control the animation +#define kAlphaWhenStopped 0.15 +#define kFadeMultiplier 0.85 + + +@interface YRKSpinningProgressIndicator () + +- (void)updateFrame:(NSTimer *)timer; +- (void)animateInBackgroundThread; +- (void)actuallyStartAnimation; +- (void)actuallyStopAnimation; +- (void)generateFinColorsStartAtPosition:(int)startPosition; + +@end + + +@implementation YRKSpinningProgressIndicator + +@synthesize color = _foreColor; +@synthesize backgroundColor = _backColor; +@synthesize drawsBackground = _drawsBackground; +@synthesize displayedWhenStopped = _displayedWhenStopped; +@synthesize usesThreadedAnimation = _usesThreadedAnimation; +@synthesize indeterminate = _isIndeterminate; +@synthesize doubleValue = _currentValue; +@synthesize maxValue = _maxValue; + + +#pragma mark Init + +- (id)initWithFrame:(NSRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + _position = 0; + _numFins = 12; +#if __has_feature(objc_arc) + _finColors = [NSMutableArray arrayWithCapacity:_numFins]; + for (int i=0; i < _numFins; i++) { + [_finColors addObject:[NSColor whiteColor]]; + } +#else + _finColors = calloc(_numFins, sizeof(NSColor*)); +#endif + + _isAnimating = NO; + _isFadingOut = NO; + + _foreColor = MB_RETAIN([NSColor blackColor]); + _backColor = MB_RETAIN([NSColor clearColor]); + _drawsBackground = NO; + + _displayedWhenStopped = YES; + _usesThreadedAnimation = YES; + + _isIndeterminate = YES; + _currentValue = 0.0; + _maxValue = 100.0; + } + return self; +} + +- (void) dealloc +{ +#if !__has_feature(objc_arc) + for (int i=0; i<_numFins; i++) { + [_finColors[i] release]; + } + free(_finColors); + [_foreColor release]; + [_backColor release]; +#endif + if (_isAnimating) [self stopAnimation:self]; +#if !__has_feature(objc_arc) + [super dealloc]; +#endif +} + +# pragma mark NSView overrides + +- (void)viewDidMoveToWindow +{ + [super viewDidMoveToWindow]; + + if ([self window] == nil) { + // No window? View hierarchy may be going away. Dispose timer to clear circular retain of timer to self to timer. + [self actuallyStopAnimation]; + } + else if (_isAnimating) { + [self actuallyStartAnimation]; + } +} + +- (void)drawRect:(NSRect)rect +{ + // Determine size based on current bounds + NSSize size = [self bounds].size; + CGFloat theMaxSize; + if(size.width >= size.height) + theMaxSize = size.height; + else + theMaxSize = size.width; + + // fill the background, if set + if(_drawsBackground) { + [_backColor set]; + [NSBezierPath fillRect:[self bounds]]; + } + + CGContextRef currentContext = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; + [NSGraphicsContext saveGraphicsState]; + + // Move the CTM so 0,0 is at the center of our bounds + CGContextTranslateCTM(currentContext,[self bounds].size.width/2,[self bounds].size.height/2); + + if (_isIndeterminate) { + NSBezierPath *path = [[NSBezierPath alloc] init]; + CGFloat lineWidth = 0.0859375 * theMaxSize; // should be 2.75 for 32x32 + CGFloat lineStart = 0.234375 * theMaxSize; // should be 7.5 for 32x32 + CGFloat lineEnd = 0.421875 * theMaxSize; // should be 13.5 for 32x32 + [path setLineWidth:lineWidth]; + [path setLineCapStyle:NSRoundLineCapStyle]; + [path moveToPoint:NSMakePoint(0,lineStart)]; + [path lineToPoint:NSMakePoint(0,lineEnd)]; + + for (int i=0; i<_numFins; i++) { + if(_isAnimating) { +#if __has_feature(objc_arc) + [(NSColor *)[_finColors objectAtIndex:i] set]; // Sets the fill and stroke colors in the current drawing context. +#else + [_finColors[i] set]; +#endif + } + else { + [[_foreColor colorWithAlphaComponent:kAlphaWhenStopped] set]; + } + + [path stroke]; + + // we draw all the fins by rotating the CTM, then just redraw the same segment again + CGContextRotateCTM(currentContext, 6.282185/_numFins); + } +#if !__has_feature(objc_arc) + [path release]; +#endif + } + else { + CGFloat lineWidth = 1 + (0.01 * theMaxSize); + CGFloat circleRadius = (theMaxSize - lineWidth) / 2.1; + NSPoint circleCenter = NSMakePoint(0, 0); + [_foreColor set]; + NSBezierPath *path = [[NSBezierPath alloc] init]; + [path setLineWidth:lineWidth]; + [path appendBezierPathWithOvalInRect:NSMakeRect(-circleRadius, -circleRadius, circleRadius*2, circleRadius*2)]; + [path stroke]; +#if !__has_feature(objc_arc) + [path release]; +#endif + path = [[NSBezierPath alloc] init]; + [path appendBezierPathWithArcWithCenter:circleCenter radius:circleRadius startAngle:90 endAngle:90-(360*(_currentValue/_maxValue)) clockwise:YES]; + [path lineToPoint:circleCenter] ; + [path fill]; +#if !__has_feature(objc_arc) + [path release]; +#endif + } + + [NSGraphicsContext restoreGraphicsState]; +} + + +#pragma mark NSProgressIndicator API + +- (void)startAnimation:(id)sender +{ + if (!_isIndeterminate) return; + if (_isAnimating && !_isFadingOut) return; + + [self actuallyStartAnimation]; +} + +- (void)stopAnimation:(id)sender +{ + // animate to stopped state + _isFadingOut = YES; +} + +/// Only the spinning style is implemented +- (void)setStyle:(NSProgressIndicatorStyle)style +{ + if (NSProgressIndicatorSpinningStyle != style) { + NSAssert(NO, @"Non-spinning styles not available."); + } +} + + +# pragma mark Custom Accessors + +- (void)setColor:(NSColor *)value +{ + if (_foreColor != value) { +#if !__has_feature(objc_arc) + [_foreColor release]; +#endif + _foreColor = MB_RETAIN(value); + + // generate all the fin colors, with the alpha components + // they already have + for (int i=0; i<_numFins; i++) { +#if __has_feature(objc_arc) + CGFloat alpha = [[_finColors objectAtIndex:i] alphaComponent]; +#else + CGFloat alpha = [_finColors[i] alphaComponent]; +#endif +#if __has_feature(objc_arc) + [_finColors replaceObjectAtIndex:i withObject:[_foreColor colorWithAlphaComponent:alpha]]; +#else + [_finColors[i] release]; + _finColors[i] = MB_RETAIN([_foreColor colorWithAlphaComponent:alpha]); +#endif + } + + [self setNeedsDisplay:YES]; + } +} + +- (void)setBackgroundColor:(NSColor *)value +{ + if (_backColor != value) { +#if !__has_feature(objc_arc) + [_backColor release]; +#endif + _backColor = MB_RETAIN(value); + [self setNeedsDisplay:YES]; + } +} + +- (void)setDrawsBackground:(BOOL)value +{ + if (_drawsBackground != value) { + _drawsBackground = value; + } + [self setNeedsDisplay:YES]; +} + +- (void)setIndeterminate:(BOOL)isIndeterminate +{ + _isIndeterminate = isIndeterminate; + if (!_isIndeterminate && _isAnimating) [self stopAnimation:self]; + [self setNeedsDisplay:YES]; +} + +- (void)setDoubleValue:(double)doubleValue +{ + // Automatically put it into determinate mode if it's not already. + if (_isIndeterminate) { + [self setIndeterminate:NO]; + } + _currentValue = doubleValue; + [self setNeedsDisplay:YES]; +} + +- (void)setMaxValue:(double)maxValue +{ + _maxValue = maxValue; + [self setNeedsDisplay:YES]; +} + +- (void)setUsesThreadedAnimation:(BOOL)useThreaded +{ + if (_usesThreadedAnimation != useThreaded) { + _usesThreadedAnimation = useThreaded; + + if (_isAnimating) { + // restart the timer to use the new mode + [self stopAnimation:self]; + [self startAnimation:self]; + } + } +} + +- (void)setDisplayedWhenStopped:(BOOL)displayedWhenStopped +{ + _displayedWhenStopped = displayedWhenStopped; + + // Show/hide ourself if necessary + if (!_isAnimating) { + if (_displayedWhenStopped && [self isHidden]) { + [self setHidden:NO]; + } + else if (!_displayedWhenStopped && ![self isHidden]) { + [self setHidden:YES]; + } + } +} + + +#pragma mark Private + +- (void)updateFrame:(NSTimer *)timer +{ + if(_position > 0) { + _position--; + } + else { + _position = _numFins - 1; + } + + // update the colors + CGFloat minAlpha = _displayedWhenStopped ? kAlphaWhenStopped : 0.01; + for (int i=0; i<_numFins; i++) { + // want each fin to fade exponentially over _numFins frames of animation +#if __has_feature(objc_arc) + CGFloat newAlpha = [[_finColors objectAtIndex:i] alphaComponent] * kFadeMultiplier; +#else + CGFloat newAlpha = [_finColors[i] alphaComponent] * kFadeMultiplier; +#endif + if (newAlpha < minAlpha) + newAlpha = minAlpha; +#if !__has_feature(objc_arc) + NSColor *oldColor = _finColors[i]; +#endif +#if __has_feature(objc_arc) + [_finColors replaceObjectAtIndex:i withObject:[_foreColor colorWithAlphaComponent:newAlpha]]; +#else + _finColors[i] = MB_RETAIN([_foreColor colorWithAlphaComponent:newAlpha]); + [oldColor release]; +#endif + } + + if (_isFadingOut) { + // check if the fadeout is done + BOOL done = YES; + for (int i=0; i<_numFins; i++) { +#if __has_feature(objc_arc) + if (fabs([[_finColors objectAtIndex:i] alphaComponent] - minAlpha) > 0.01) { + done = NO; + break; + } +#else + if (fabs([_finColors[i] alphaComponent] - minAlpha) > 0.01) { + done = NO; + break; + } +#endif + } + if (done) { + [self actuallyStopAnimation]; + } + } + else { + // "light up" the next fin (with full alpha) +#if !__has_feature(objc_arc) + NSColor *oldColor = _finColors[_position]; +#endif +#if __has_feature(objc_arc) + [_finColors replaceObjectAtIndex:_position withObject:_foreColor]; +#else + _finColors[_position] = MB_RETAIN(_foreColor); + [oldColor release]; +#endif + } + + if (_usesThreadedAnimation) { + // draw now instead of waiting for setNeedsDisplay (that's the whole reason + // we're animating from background thread) + [self display]; + } + else { + [self setNeedsDisplay:YES]; + } +} + +- (void)actuallyStartAnimation +{ + // Just to be safe kill any existing timer. + [self actuallyStopAnimation]; + + _isAnimating = YES; + _isFadingOut = NO; + + // always start from the top + _position = 1; + + if (!_displayedWhenStopped) + [self setHidden:NO]; + + if ([self window]) { + // Why animate if not visible? viewDidMoveToWindow will re-call this method when needed. + if (_usesThreadedAnimation) { + _animationThread = [[NSThread alloc] initWithTarget:self selector:@selector(animateInBackgroundThread) object:nil]; + [_animationThread start]; + } + else { + _animationTimer = MB_RETAIN([NSTimer timerWithTimeInterval:(NSTimeInterval)0.05 + target:self + selector:@selector(updateFrame:) + userInfo:nil + repeats:YES]); + + [[NSRunLoop currentRunLoop] addTimer:_animationTimer forMode:NSRunLoopCommonModes]; + [[NSRunLoop currentRunLoop] addTimer:_animationTimer forMode:NSDefaultRunLoopMode]; + [[NSRunLoop currentRunLoop] addTimer:_animationTimer forMode:NSEventTrackingRunLoopMode]; + } + } +} + +- (void)actuallyStopAnimation +{ + _isAnimating = NO; + _isFadingOut = NO; + + if (!_displayedWhenStopped) + [self setHidden:YES]; + + if (_animationThread) { + // we were using threaded animation + [_animationThread cancel]; + if (![_animationThread isFinished]) { + [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; + } +#if !__has_feature(objc_arc) + [_animationThread release]; +#endif + _animationThread = nil; + } + else if (_animationTimer) { + // we were using timer-based animation + [_animationTimer invalidate]; +#if !__has_feature(objc_arc) + [_animationTimer release]; +#endif + _animationTimer = nil; + } + [self setNeedsDisplay:YES]; +} + +- (void)generateFinColorsStartAtPosition:(int)startPosition +{ + for (int i=0; i<_numFins; i++) { +#if __has_feature(objc_arc) + NSColor *oldColor = [_finColors objectAtIndex:i]; +#else + NSColor *oldColor = _finColors[i]; +#endif + CGFloat alpha = [oldColor alphaComponent]; +#if __has_feature(objc_arc) + [_finColors replaceObjectAtIndex:i withObject:[_foreColor colorWithAlphaComponent:alpha]]; +#else + _finColors[i] = MB_RETAIN([_foreColor colorWithAlphaComponent:alpha]); + [oldColor release]; +#endif + } +} + +- (void)animateInBackgroundThread +{ +#if __has_feature(objc_arc) + @autoreleasepool { +#else + NSAutoreleasePool *animationPool = [[NSAutoreleasePool alloc] init]; +#endif + // Set up the animation speed to subtly change with size > 32. + // int animationDelay = 38000 + (2000 * ([self bounds].size.height / 32)); + + // Set the rev per minute here + int omega = 100; // RPM + int animationDelay = 60*1000000/omega/_numFins; + int poolFlushCounter = 0; + + do { + [self updateFrame:nil]; + usleep(animationDelay); + poolFlushCounter++; + if (poolFlushCounter > 256) { +#if !__has_feature(objc_arc) + [animationPool drain]; + animationPool = [[NSAutoreleasePool alloc] init]; +#endif + poolFlushCounter = 0; + } + } while (![[NSThread currentThread] isCancelled]); +#if __has_feature(objc_arc) + } +#else + [animationPool release]; +#endif +} + +@end + +#endif // !(TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE) diff --git a/AppleParty/AppleParty/Vendors/QrcodeUtil.h b/AppleParty/AppleParty/Vendors/QrcodeUtil.h new file mode 100644 index 0000000..8521a8d --- /dev/null +++ b/AppleParty/AppleParty/Vendors/QrcodeUtil.h @@ -0,0 +1,16 @@ +// +// QrcodeUtil.h +// +// Created by HTC on 2019/11/25. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSDictionary* scanQRCodeOnScreen(); + +NSImage* createQRImage(NSString *string, NSSize size); + +NS_ASSUME_NONNULL_END diff --git a/AppleParty/AppleParty/Vendors/QrcodeUtil.m b/AppleParty/AppleParty/Vendors/QrcodeUtil.m new file mode 100644 index 0000000..68fa03a --- /dev/null +++ b/AppleParty/AppleParty/Vendors/QrcodeUtil.m @@ -0,0 +1,102 @@ +// +// QrcodeUtil.m +// +// Created by HTC on 2019/11/25. +// + +#import +#import +#import + +NSDictionary* scanQRCodeOnScreen() { + /* displays[] Quartz display ID's */ + CGDirectDisplayID *displays = nil; + + CGError err = CGDisplayNoErr; + CGDisplayCount dspCount = 0; + + /* How many active displays do we have? */ + err = CGGetActiveDisplayList(0, NULL, &dspCount); + + /* If we are getting an error here then their won't be much to display. */ + if(err != CGDisplayNoErr) + { + NSLog(@"Could not get active display count (%d)\n", err); + return @{@"qrcode": @[], @"message": [NSString stringWithFormat:@"Could not get active display count (%d).", err]};; + } + + /* Allocate enough memory to hold all the display IDs we have. */ + displays = calloc((size_t)dspCount, sizeof(CGDirectDisplayID)); + + // Get the list of active displays + err = CGGetActiveDisplayList(dspCount, + displays, + &dspCount); + + /* More error-checking here. */ + if(err != CGDisplayNoErr) + { + NSLog(@"Could not get active display list (%d)\n", err); + return @{@"qrcode": @[], @"message": [NSString stringWithFormat:@"Could not get active display list (%d).", err]};; + } + + NSMutableArray* foundSSUrls = [NSMutableArray array]; + + CIDetector *detector = [CIDetector detectorOfType:@"CIDetectorTypeQRCode" + context:nil + options:@{ CIDetectorAccuracy:CIDetectorAccuracyHigh }]; + + for (unsigned int displaysIndex = 0; displaysIndex < dspCount; displaysIndex++) + { + /* Make a snapshot image of the current display. */ + CGImageRef image = CGDisplayCreateImage(displays[displaysIndex]); + NSArray *features = [detector featuresInImage:[CIImage imageWithCGImage:image]]; + for (CIQRCodeFeature *feature in features) { + [foundSSUrls addObject:feature.messageString]; + } + CGImageRelease(image); + } + + free(displays); + + return @{@"qrcode": foundSSUrls, @"message": @"success"}; +} + +NSImage* createQRImage(NSString *string, NSSize size) { + NSImage *outputImage = [[NSImage alloc]initWithSize:size]; + [outputImage lockFocus]; + + // Setup the QR filter with our string + CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"]; + [filter setDefaults]; + + NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; + [filter setValue:data forKey:@"inputMessage"]; + /* + L: 7% + M: 15% + Q: 25% + H: 30% + */ + [filter setValue:@"Q" forKey:@"inputCorrectionLevel"]; + + CIImage *image = [filter valueForKey:@"outputImage"]; + + // Calculate the size of the generated image and the scale for the desired image size + CGRect extent = CGRectIntegral(image.extent); + CGFloat scale = MIN(size.width / CGRectGetWidth(extent), size.height / CGRectGetHeight(extent)); + + CGImageRef bitmapImage = [NSGraphicsContext.currentContext.CIContext createCGImage:image fromRect:extent]; + + CGContextRef graphicsContext = NSGraphicsContext.currentContext.CGContext; + + CGContextSetInterpolationQuality(graphicsContext, kCGInterpolationNone); + CGContextScaleCTM(graphicsContext, scale, scale); + CGContextDrawImage(graphicsContext, extent, bitmapImage); + + // Cleanup + CGImageRelease(bitmapImage); + + [outputImage unlockFocus]; + return outputImage; +} diff --git a/AppleParty/AppleParty/VerifyReceipt/APVerifyReceipt.storyboard b/AppleParty/AppleParty/VerifyReceipt/APVerifyReceipt.storyboard new file mode 100644 index 0000000..c345e87 --- /dev/null +++ b/AppleParty/AppleParty/VerifyReceipt/APVerifyReceipt.storyboard @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppleParty/AppleParty/VerifyReceipt/APVerifyReceiptVC.swift b/AppleParty/AppleParty/VerifyReceipt/APVerifyReceiptVC.swift new file mode 100644 index 0000000..c022db5 --- /dev/null +++ b/AppleParty/AppleParty/VerifyReceipt/APVerifyReceiptVC.swift @@ -0,0 +1,84 @@ +// +// APVerifyReceiptVC.swift +// AppleParty +// +// Created by HTC on 2022/6/1. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import Cocoa + +class APVerifyReceiptVC: NSViewController { + + @IBOutlet weak var sharedSecretField: NSTextField! + @IBOutlet weak var apiTypeMatrix: NSMatrix! + @IBOutlet weak var receiptTextView: NSTextView! + @IBOutlet weak var responseTextView: NSTextView! + @IBOutlet weak var verifyButton: NSButton! + + + override func viewDidLoad() { + super.viewDidLoad() + } + + + @IBAction func clickedVerifyButtion(_ sender: Any) { + + let receiptString = receiptTextView.string + if receiptString.isEmpty { + APHUD.hide(message: "请填写 receipt base64 字符串~", delayTime: 1) + return + } + + responseTextView.string = "" + APHUD.show(message: "请求中...") + + let sharedSecretString = sharedSecretField.stringValue + var apiUrl = "" + let apiType = apiTypeMatrix.selectedRow + if apiType == 0 { + apiUrl = "https://sandbox.itunes.apple.com/verifyReceipt" + } + else if (apiType == 1) { + apiUrl = "https://buy.itunes.apple.com/verifyReceipt" + } + + // unused : exclude-old-transactions + var json: [String: Any] = ["receipt-data": receiptString] + if !sharedSecretString.isEmpty { + json["password"] = sharedSecretString + } + let jsonData = try? JSONSerialization.data(withJSONObject: json) + + // create post request + let url = URL(string: apiUrl)! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.timeoutInterval = 15 + request.setValue("\(String(describing: jsonData?.count))", forHTTPHeaderField: "Content-Length") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = jsonData + + let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in + APHUD.hide() + guard let data = data, error == nil else { + let msg = error?.localizedDescription ?? "未知错误" + APHUD.hide(message: msg, delayTime: 1) + print(msg) + return + } + let responseJSON = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) + if let responseJSON = responseJSON as? [String: Any] { + DispatchQueue.main.async { + self?.responseTextView.string = String(format: "\(responseJSON)") + } + } else { + APHUD.hide(message: "数据解析错误~", delayTime: 1) + } + } + + task.resume() + + } + +} diff --git a/AppleParty/AppleParty/en.lproj/InfoPlist.strings b/AppleParty/AppleParty/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..b2c115e --- /dev/null +++ b/AppleParty/AppleParty/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + AppleParty + + Created by HTC on 2022/3/10. + Copyright © 2022 37 Mobile Games. All rights reserved. +*/ diff --git a/AppleParty/AppleParty/en.lproj/Localizable.strings b/AppleParty/AppleParty/en.lproj/Localizable.strings new file mode 100644 index 0000000..8674bb4 --- /dev/null +++ b/AppleParty/AppleParty/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + AppleParty + + Created by HTC on 2022/3/10. + Copyright © 2022 37 Mobile Games. All rights reserved. +*/ diff --git a/AppleParty/AppleParty/zh-Hans.lproj/InfoPlist.strings b/AppleParty/AppleParty/zh-Hans.lproj/InfoPlist.strings new file mode 100644 index 0000000..16cb7e5 --- /dev/null +++ b/AppleParty/AppleParty/zh-Hans.lproj/InfoPlist.strings @@ -0,0 +1,10 @@ +/* + InfoPlist.strings + AppleParty + + Created by HTC on 2022/3/10. + Copyright © 2022 37 Mobile Games. All rights reserved. +*/ + + +"CFBundleName" = "苹果派"; diff --git a/AppleParty/AppleParty/zh-Hans.lproj/Localizable.strings b/AppleParty/AppleParty/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000..8674bb4 --- /dev/null +++ b/AppleParty/AppleParty/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + AppleParty + + Created by HTC on 2022/3/10. + Copyright © 2022 37 Mobile Games. All rights reserved. +*/ diff --git a/AppleParty/ApplePartyTests/ApplePartyTests.swift b/AppleParty/ApplePartyTests/ApplePartyTests.swift new file mode 100644 index 0000000..72156b4 --- /dev/null +++ b/AppleParty/ApplePartyTests/ApplePartyTests.swift @@ -0,0 +1,37 @@ +// +// ApplePartyTests.swift +// ApplePartyTests +// +// Created by HTC on 2022/3/10. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import XCTest +@testable import AppleParty + +class ApplePartyTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/AppleParty/ApplePartyUITests/ApplePartyUITests.swift b/AppleParty/ApplePartyUITests/ApplePartyUITests.swift new file mode 100644 index 0000000..fbae285 --- /dev/null +++ b/AppleParty/ApplePartyUITests/ApplePartyUITests.swift @@ -0,0 +1,43 @@ +// +// ApplePartyUITests.swift +// ApplePartyUITests +// +// Created by HTC on 2022/3/10. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import XCTest + +class ApplePartyUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/AppleParty/ApplePartyUITests/ApplePartyUITestsLaunchTests.swift b/AppleParty/ApplePartyUITests/ApplePartyUITestsLaunchTests.swift new file mode 100644 index 0000000..6f0b885 --- /dev/null +++ b/AppleParty/ApplePartyUITests/ApplePartyUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// ApplePartyUITestsLaunchTests.swift +// ApplePartyUITests +// +// Created by HTC on 2022/3/10. +// Copyright © 2022 37 Mobile Games. All rights reserved. +// + +import XCTest + +class ApplePartyUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/AppleParty/LICENSE b/AppleParty/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/AppleParty/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/AppleParty/Podfile b/AppleParty/Podfile new file mode 100644 index 0000000..8a1bba4 --- /dev/null +++ b/AppleParty/Podfile @@ -0,0 +1,35 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'AppleParty' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for AppleParty + pod 'Alamofire', '~> 5.9.1' + pod 'SnapKit', '~> 5.0' + pod 'Sparkle', '~> 2.6.3' + pod 'Kanna', '~> 5.2' + pod 'SWXMLHash', '~> 5.0' + pod 'CoreXLSX', '~> 0.14' + pod 'ExpandingDatePicker', '~> 1.0' + pod 'KeychainAccess', '~> 4.2' + + target 'ApplePartyTests' do + inherit! :search_paths + # Pods for testing + end + + target 'ApplePartyUITests' do + # Pods for testing + end + +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '11.0' + end + end +end diff --git a/AppleParty/README.md b/AppleParty/README.md new file mode 100644 index 0000000..bdafa88 --- /dev/null +++ b/AppleParty/README.md @@ -0,0 +1,132 @@ + +## Apple Party(苹果派) + +AppleParty.png + + +### 一、App 介绍 + +AppleParty 是三七互娱旗下37手游 iOS 团队研发,实现快速操作 App Store Connect 后台的自动化 macOS 工具。 + + +**使用和原理介绍:** +- [AppleParty(苹果派)v3 支持 App Store 新定价机制 - 批量配置自定价格和销售范围](https://juejin.cn/post/7226327556198744122) +- [开源一款苹果 macOS 工具 - AppleParty(苹果派)](https://juejin.cn/post/7081069026515877919) +- [使用 App Store Connect API 批量创建内购商品 ](https://juejin.cn/post/7181099247956131896) + +**支持功能** + +- 内购买项目管理(批量创建和更新); +- ~~批量商店图和预览视频上传和更新~~(苹果2023年禁止使用 XML feed 上传,ASC API 上传方式敬请期待~); +- 邮件发送工具; +- 二维码扫描和生成工具; +- 上传 iap 文件; + + +**TODO** + +- 数据报表下载; +- 元数据管理; +- 开发证书管理; +- 更多功能,敬请期待~ + + +### 二、项目背景 + +目前,iOS/macOS App 上架 App Store,与苹果打交道的唯一方式,就是登陆苹果 App Store Connect 后台([https://appstoreconnect.apple.com](https://appstoreconnect.apple.com),通过苹果后台进行 App 所有的信息和素材等送审准备工作。但是,目前苹果后台的自动化水平还处于零基础,很多重复的操作和功能,都没有提供批量处理方案,比如: + +- 商店截图和预览视频的上传 +- 应用内购商品的创建和更新 +- App 本地化的元数据信息配置 +- ... + + +App 分析的指标: + +- 展示次数 +- 产品页面查看次数 +- 首次下载次数 +- 净预订量 +- 平台版本(iOS14.5、iOS15...) +- 页面类型(产品页面、商店表单、App内活动...) +- 用户来源(网页引荐来源、App 引荐来源、AppStore 浏览、AppStore 搜索、活动通知...) +- ... + +以上的 App 分析数据,每次只能下载一个指标的数据,每个 App 有十几个指标,操作这些重复的配置往往占用了运营同学非常长的时间,效率低且重复无聊的工作,导致我们长期无法做更多的时间开启和享受创造性。 + +基于以上种种痛点,我们从多个技术手段,打造了 Apple Party(苹果派对)工具! 通过尽可能快速实现操作的自动化流程,从而大大提高苹果后台的操作效率! + + + +**Apple Party(苹果派)** + +我们倡导工作之余,丰富多彩的生活要领,健身、旅游、聚会、培养艺术兴趣等等。 + +- Party:派对 即 “宴会,聚会” 的意思,大家聚在一起庆祝和休闲的一种活动。 + +所以,Apple Party(苹果派对),简称:**苹果派**,就是希望大家在使用苹果的服务时,像似参加一场苹果派对,尽情欢乐,欢聚宴会~ + + +### 三、安装说明 + +#### 下载安装包 + +- [Releases](https://github.com/37iOS/AppleParty/releases) + + +**update 更新** + +项目使用 Sparkle 来更新,重启就会自动检查更新。也可以手动检查更新。 + + +#### 手动构建 +**build 构建** + +在项目主目录下,执行安装依赖库命令: +``` +pod install +``` + +然后双击 `AppleParty.xcworkspace` 打开项目构建。 + + +> 项目依赖 Swift Package,Xcode 可能下载失败,可以清理缓存试试: 打开 Xcode File -> Packages -> Reset Package Caches + +### 四、FAQ + +- [AppleParty 使用说明 - wiki](https://github.com/37iOS/AppleParty/wiki/AppleParty-%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E) + +- [New Issue](https://github.com/37iOS/AppleParty/issues/new/choose) + + +### 五、效果示例 + +screenshot/01.png +screenshot/02.png +screenshot/03.png +screenshot/04.png +screenshot/05.png +screenshot/06.png +screenshot/07.png +screenshot/08.png +screenshot/09.png +screenshot/10.png + + + +### 六、特别感谢 + +- [Alamofire/Alamofire](https://github.com/Alamofire/Alamofire) +- [Kitura/Swift-SMTP](https://github.com/Kitura/Swift-SMTP) +- [SnapKit/SnapKit](https://github.com/SnapKit/SnapKit) +- [sparkle-project/Sparkle](https://github.com/sparkle-project/Sparkle) +- [tid-kijyun/Kanna](https://github.com/tid-kijyun/Kanna) +- [drmohundro/SWXMLHash](https://github.com/drmohundro/SWXMLHash) +- [jdg/MBProgressHUD](https://github.com/jdg/MBProgressHUD) +- [joshuajylin/MBProgressHUD-macOS](https://github.com/joshuajylin/MBProgressHUD-macOS) +- [Yueoaix/SymbolicatorX](https://github.com/Yueoaix/SymbolicatorX) +- [fpotter/ExpandingDatePicker](https://github.com/fpotter/ExpandingDatePicker) +- [kishikawakatsumi/KeychainAccess](https://github.com/kishikawakatsumi/KeychainAccess) +- [AvdLee/appstoreconnect-swift-sdk](https://github.com/AvdLee/appstoreconnect-swift-sdk) + + diff --git a/AppleParty/icon.png b/AppleParty/icon.png new file mode 100644 index 0000000..296f8d1 Binary files /dev/null and b/AppleParty/icon.png differ diff --git a/AppleParty/screenshot/01.png b/AppleParty/screenshot/01.png new file mode 100644 index 0000000..97dcb62 Binary files /dev/null and b/AppleParty/screenshot/01.png differ diff --git a/AppleParty/screenshot/02.png b/AppleParty/screenshot/02.png new file mode 100644 index 0000000..964a806 Binary files /dev/null and b/AppleParty/screenshot/02.png differ diff --git a/AppleParty/screenshot/03.png b/AppleParty/screenshot/03.png new file mode 100644 index 0000000..7930cb9 Binary files /dev/null and b/AppleParty/screenshot/03.png differ diff --git a/AppleParty/screenshot/04.png b/AppleParty/screenshot/04.png new file mode 100644 index 0000000..3465743 Binary files /dev/null and b/AppleParty/screenshot/04.png differ diff --git a/AppleParty/screenshot/05.png b/AppleParty/screenshot/05.png new file mode 100644 index 0000000..db65121 Binary files /dev/null and b/AppleParty/screenshot/05.png differ diff --git a/AppleParty/screenshot/06.png b/AppleParty/screenshot/06.png new file mode 100644 index 0000000..6fa57f7 Binary files /dev/null and b/AppleParty/screenshot/06.png differ diff --git a/AppleParty/screenshot/07.png b/AppleParty/screenshot/07.png new file mode 100644 index 0000000..4b88180 Binary files /dev/null and b/AppleParty/screenshot/07.png differ diff --git a/AppleParty/screenshot/08.png b/AppleParty/screenshot/08.png new file mode 100644 index 0000000..d8d2c43 Binary files /dev/null and b/AppleParty/screenshot/08.png differ diff --git a/AppleParty/screenshot/09.png b/AppleParty/screenshot/09.png new file mode 100644 index 0000000..1cae774 Binary files /dev/null and b/AppleParty/screenshot/09.png differ diff --git a/AppleParty/screenshot/10.png b/AppleParty/screenshot/10.png new file mode 100644 index 0000000..d1c9a36 Binary files /dev/null and b/AppleParty/screenshot/10.png differ diff --git a/IAP-20251024_142831.xlsx b/IAP-20251024_142831.xlsx new file mode 100644 index 0000000..dbf1994 Binary files /dev/null and b/IAP-20251024_142831.xlsx differ