├── .gitattributes ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .swiftformat ├── .swiftlint.yml ├── Gemfile ├── Gemfile.lock ├── README.md ├── SimpleDesktops.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── SimpleDesktops.xcscheme │ └── WallpaperWidgetExtension.xcscheme ├── SimpleDesktops ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── AppIcon-128.png │ │ ├── AppIcon-128@2x.png │ │ ├── AppIcon-16.png │ │ ├── AppIcon-16@2x.png │ │ ├── AppIcon-256.png │ │ ├── AppIcon-256@2x.png │ │ ├── AppIcon-32.png │ │ ├── AppIcon-32@2x.png │ │ ├── AppIcon-512.png │ │ ├── AppIcon-512@2x.png │ │ └── Contents.json │ └── Contents.json ├── Info.plist ├── Models │ ├── ChangeInterval.swift │ ├── Options.swift │ └── SDPictureInfo.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SimpleDesktops.entitlements ├── SimpleDesktops.xcdatamodeld │ ├── .xccurrentversion │ └── SimpleDesktops.xcdatamodel │ │ └── contents ├── SimpleDesktopsApp.swift ├── SimpleDesktopsRequest.swift ├── Utilities │ ├── Logger.swift │ ├── PersistenceController.swift │ ├── UserNotification.swift │ └── WallpaperManager.swift ├── ViewModels │ ├── Picture.swift │ ├── PictureService.swift │ └── Preferences.swift ├── Views │ ├── HistoryView.swift │ ├── Modifiers │ │ ├── CapsuledButtonStyle.swift │ │ └── ImageButtonStyle.swift │ ├── PopoverView.swift │ ├── PreferenceView.swift │ └── PreviewView.swift ├── en.lproj │ └── Localizable.strings └── zh-Hans.lproj │ └── Localizable.strings ├── SimpleDesktopsTests ├── Info.plist ├── Mock │ └── MockURLProtocol.swift ├── OptionsTests.swift ├── PictureTests.swift ├── SimpleDesktopsRequestTests.swift └── TestData │ └── SimpleDesktopsRequestTests.html ├── WallpaperWidget ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── WidgetBackground.colorset │ │ └── Contents.json ├── Info.plist ├── WallpaperWidget.entitlements └── WallpaperWidget.swift └── fastlane ├── Appfile.swift ├── Fastfile.swift ├── Pluginfile ├── Scanfile └── swift ├── Actions.swift ├── Appfile.swift ├── ArgumentProcessor.swift ├── Atomic.swift ├── ControlCommand.swift ├── Deliverfile.swift ├── DeliverfileProtocol.swift ├── Fastfile.swift ├── Fastlane.swift ├── FastlaneSwiftRunner ├── FastlaneSwiftRunner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── FastlaneRunner.xcscheme └── README.txt ├── Gymfile.swift ├── GymfileProtocol.swift ├── LaneFileProtocol.swift ├── MainProcess.swift ├── Matchfile.swift ├── MatchfileProtocol.swift ├── OptionalConfigValue.swift ├── Plugins.swift ├── Precheckfile.swift ├── PrecheckfileProtocol.swift ├── RubyCommand.swift ├── RubyCommandable.swift ├── Runner.swift ├── RunnerArgument.swift ├── Scanfile.swift ├── ScanfileProtocol.swift ├── Screengrabfile.swift ├── ScreengrabfileProtocol.swift ├── Snapshotfile.swift ├── SnapshotfileProtocol.swift ├── SocketClient.swift ├── SocketClientDelegateProtocol.swift ├── SocketResponse.swift ├── formatting ├── Brewfile ├── Brewfile.lock.json └── Rakefile ├── main.swift └── upgrade_manifest.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Exclude test data files from stats 5 | SimpleDesktopsTests/TestData/* linguist-vendored 6 | 7 | # Exclude fastlane files from stats 8 | fastlane/* linguist-vendored 9 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: macos-12 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 0 12 | 13 | - name: Install the Apple certificate 14 | env: 15 | BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} 16 | P12_PASSWORD: "" 17 | KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} 18 | run: | 19 | # create variables 20 | CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 21 | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db 22 | 23 | # import certificate and provisioning profile from secrets 24 | echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH 25 | 26 | # create temporary keychain 27 | security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 28 | security set-keychain-settings -lut 21600 $KEYCHAIN_PATH 29 | security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 30 | 31 | # import certificate to keychain 32 | security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH 33 | security list-keychain -d user -s $KEYCHAIN_PATH 34 | 35 | - name: Get the version 36 | id: vars 37 | run: echo ::set-output name=VERSION::$(git describe --tags --abbrev=0 | cut -c 2-) 38 | 39 | - name: Build 40 | uses: maierj/fastlane-action@v2.1.0 41 | with: 42 | lane: "release" 43 | options: '{ "version": "${{ steps.vars.outputs.VERSION }}", "build": "${GITHUB_RUN_NUMBER}" }' 44 | 45 | - name: Submit to release 46 | uses: softprops/action-gh-release@v1 47 | with: 48 | files: ./.build/SimpleDesktops_v${{ steps.vars.outputs.VERSION }}.dmg 49 | tag_name: v${{ steps.vars.outputs.VERSION }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | fastlane/FastlaneRunner 85 | 86 | # Code Injection 87 | # 88 | # After new code Injection tools there's a generated folder /iOSInjectionProject 89 | # https://github.com/johnno1962/injectionforxcode 90 | 91 | iOSInjectionProject/ 92 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --swiftversion 5.0 2 | 3 | --exclude fastlane 4 | 5 | --maxwidth 100 6 | 7 | --disable wrapMultilineStatementBraces 8 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - fastlane 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | gem "xcode-install" 5 | 6 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 7 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.5) 5 | rexml 6 | addressable (2.8.0) 7 | public_suffix (>= 2.0.2, < 5.0) 8 | artifactory (3.0.15) 9 | atomos (0.1.3) 10 | aws-eventstream (1.2.0) 11 | aws-partitions (1.614.0) 12 | aws-sdk-core (3.131.6) 13 | aws-eventstream (~> 1, >= 1.0.2) 14 | aws-partitions (~> 1, >= 1.525.0) 15 | aws-sigv4 (~> 1.1) 16 | jmespath (~> 1, >= 1.6.1) 17 | aws-sdk-kms (1.58.0) 18 | aws-sdk-core (~> 3, >= 3.127.0) 19 | aws-sigv4 (~> 1.1) 20 | aws-sdk-s3 (1.114.0) 21 | aws-sdk-core (~> 3, >= 3.127.0) 22 | aws-sdk-kms (~> 1) 23 | aws-sigv4 (~> 1.4) 24 | aws-sigv4 (1.5.1) 25 | aws-eventstream (~> 1, >= 1.0.2) 26 | babosa (1.0.4) 27 | claide (1.1.0) 28 | colored (1.2) 29 | colored2 (3.1.2) 30 | commander (4.6.0) 31 | highline (~> 2.0.0) 32 | declarative (0.0.20) 33 | digest-crc (0.6.4) 34 | rake (>= 12.0.0, < 14.0.0) 35 | domain_name (0.5.20190701) 36 | unf (>= 0.0.5, < 1.0.0) 37 | dotenv (2.8.1) 38 | emoji_regex (3.2.3) 39 | excon (0.92.4) 40 | faraday (1.10.0) 41 | faraday-em_http (~> 1.0) 42 | faraday-em_synchrony (~> 1.0) 43 | faraday-excon (~> 1.1) 44 | faraday-httpclient (~> 1.0) 45 | faraday-multipart (~> 1.0) 46 | faraday-net_http (~> 1.0) 47 | faraday-net_http_persistent (~> 1.0) 48 | faraday-patron (~> 1.0) 49 | faraday-rack (~> 1.0) 50 | faraday-retry (~> 1.0) 51 | ruby2_keywords (>= 0.0.4) 52 | faraday-cookie_jar (0.0.7) 53 | faraday (>= 0.8.0) 54 | http-cookie (~> 1.0.0) 55 | faraday-em_http (1.0.0) 56 | faraday-em_synchrony (1.0.0) 57 | faraday-excon (1.1.0) 58 | faraday-httpclient (1.0.1) 59 | faraday-multipart (1.0.4) 60 | multipart-post (~> 2) 61 | faraday-net_http (1.0.1) 62 | faraday-net_http_persistent (1.2.0) 63 | faraday-patron (1.0.0) 64 | faraday-rack (1.0.0) 65 | faraday-retry (1.0.3) 66 | faraday_middleware (1.2.0) 67 | faraday (~> 1.0) 68 | fastimage (2.2.6) 69 | fastlane (2.208.0) 70 | CFPropertyList (>= 2.3, < 4.0.0) 71 | addressable (>= 2.8, < 3.0.0) 72 | artifactory (~> 3.0) 73 | aws-sdk-s3 (~> 1.0) 74 | babosa (>= 1.0.3, < 2.0.0) 75 | bundler (>= 1.12.0, < 3.0.0) 76 | colored 77 | commander (~> 4.6) 78 | dotenv (>= 2.1.1, < 3.0.0) 79 | emoji_regex (>= 0.1, < 4.0) 80 | excon (>= 0.71.0, < 1.0.0) 81 | faraday (~> 1.0) 82 | faraday-cookie_jar (~> 0.0.6) 83 | faraday_middleware (~> 1.0) 84 | fastimage (>= 2.1.0, < 3.0.0) 85 | gh_inspector (>= 1.1.2, < 2.0.0) 86 | google-apis-androidpublisher_v3 (~> 0.3) 87 | google-apis-playcustomapp_v1 (~> 0.1) 88 | google-cloud-storage (~> 1.31) 89 | highline (~> 2.0) 90 | json (< 3.0.0) 91 | jwt (>= 2.1.0, < 3) 92 | mini_magick (>= 4.9.4, < 5.0.0) 93 | multipart-post (~> 2.0.0) 94 | naturally (~> 2.2) 95 | optparse (~> 0.1.1) 96 | plist (>= 3.1.0, < 4.0.0) 97 | rubyzip (>= 2.0.0, < 3.0.0) 98 | security (= 0.1.3) 99 | simctl (~> 1.6.3) 100 | terminal-notifier (>= 2.0.0, < 3.0.0) 101 | terminal-table (>= 1.4.5, < 2.0.0) 102 | tty-screen (>= 0.6.3, < 1.0.0) 103 | tty-spinner (>= 0.8.0, < 1.0.0) 104 | word_wrap (~> 1.0.0) 105 | xcodeproj (>= 1.13.0, < 2.0.0) 106 | xcpretty (~> 0.3.0) 107 | xcpretty-travis-formatter (>= 0.0.3) 108 | fastlane-plugin-dmg (0.1.1) 109 | gh_inspector (1.1.3) 110 | google-apis-androidpublisher_v3 (0.25.0) 111 | google-apis-core (>= 0.7, < 2.a) 112 | google-apis-core (0.7.0) 113 | addressable (~> 2.5, >= 2.5.1) 114 | googleauth (>= 0.16.2, < 2.a) 115 | httpclient (>= 2.8.1, < 3.a) 116 | mini_mime (~> 1.0) 117 | representable (~> 3.0) 118 | retriable (>= 2.0, < 4.a) 119 | rexml 120 | webrick 121 | google-apis-iamcredentials_v1 (0.13.0) 122 | google-apis-core (>= 0.7, < 2.a) 123 | google-apis-playcustomapp_v1 (0.10.0) 124 | google-apis-core (>= 0.7, < 2.a) 125 | google-apis-storage_v1 (0.18.0) 126 | google-apis-core (>= 0.7, < 2.a) 127 | google-cloud-core (1.6.0) 128 | google-cloud-env (~> 1.0) 129 | google-cloud-errors (~> 1.0) 130 | google-cloud-env (1.6.0) 131 | faraday (>= 0.17.3, < 3.0) 132 | google-cloud-errors (1.2.0) 133 | google-cloud-storage (1.37.0) 134 | addressable (~> 2.8) 135 | digest-crc (~> 0.4) 136 | google-apis-iamcredentials_v1 (~> 0.1) 137 | google-apis-storage_v1 (~> 0.1) 138 | google-cloud-core (~> 1.6) 139 | googleauth (>= 0.16.2, < 2.a) 140 | mini_mime (~> 1.0) 141 | googleauth (1.2.0) 142 | faraday (>= 0.17.3, < 3.a) 143 | jwt (>= 1.4, < 3.0) 144 | memoist (~> 0.16) 145 | multi_json (~> 1.11) 146 | os (>= 0.9, < 2.0) 147 | signet (>= 0.16, < 2.a) 148 | highline (2.0.3) 149 | http-cookie (1.0.5) 150 | domain_name (~> 0.5) 151 | httpclient (2.8.3) 152 | jmespath (1.6.1) 153 | json (2.6.2) 154 | jwt (2.4.1) 155 | memoist (0.16.2) 156 | mini_magick (4.11.0) 157 | mini_mime (1.1.2) 158 | multi_json (1.15.0) 159 | multipart-post (2.0.0) 160 | nanaimo (0.3.0) 161 | naturally (2.2.1) 162 | optparse (0.1.1) 163 | os (1.1.4) 164 | plist (3.6.0) 165 | public_suffix (4.0.7) 166 | rake (13.0.6) 167 | representable (3.2.0) 168 | declarative (< 0.1.0) 169 | trailblazer-option (>= 0.1.1, < 0.2.0) 170 | uber (< 0.2.0) 171 | retriable (3.1.2) 172 | rexml (3.2.5) 173 | rouge (2.0.7) 174 | ruby2_keywords (0.0.5) 175 | rubyzip (2.3.2) 176 | security (0.1.3) 177 | signet (0.17.0) 178 | addressable (~> 2.8) 179 | faraday (>= 0.17.5, < 3.a) 180 | jwt (>= 1.5, < 3.0) 181 | multi_json (~> 1.10) 182 | simctl (1.6.8) 183 | CFPropertyList 184 | naturally 185 | terminal-notifier (2.0.0) 186 | terminal-table (1.8.0) 187 | unicode-display_width (~> 1.1, >= 1.1.1) 188 | trailblazer-option (0.1.2) 189 | tty-cursor (0.7.1) 190 | tty-screen (0.8.1) 191 | tty-spinner (0.9.3) 192 | tty-cursor (~> 0.7) 193 | uber (0.1.0) 194 | unf (0.1.4) 195 | unf_ext 196 | unf_ext (0.0.8.2) 197 | unicode-display_width (1.8.0) 198 | webrick (1.7.0) 199 | word_wrap (1.0.0) 200 | xcode-install (2.8.1) 201 | claide (>= 0.9.1) 202 | fastlane (>= 2.1.0, < 3.0.0) 203 | xcodeproj (1.22.0) 204 | CFPropertyList (>= 2.3.3, < 4.0) 205 | atomos (~> 0.1.3) 206 | claide (>= 1.0.2, < 2.0) 207 | colored2 (~> 3.1) 208 | nanaimo (~> 0.3.0) 209 | rexml (~> 3.2.4) 210 | xcpretty (0.3.0) 211 | rouge (~> 2.0.7) 212 | xcpretty-travis-formatter (1.0.1) 213 | xcpretty (~> 0.2, >= 0.0.7) 214 | 215 | PLATFORMS 216 | arm64-darwin-21 217 | 218 | DEPENDENCIES 219 | fastlane 220 | fastlane-plugin-dmg 221 | xcode-install 222 | 223 | BUNDLED WITH 224 | 2.3.11 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

18 | 19 | ## Simple Desktops 20 | 21 | Customize your Mac's desktop with beautiful wallpapers from [Simple Desktops](http://simpledesktops.com). 22 | -------------------------------------------------------------------------------- /SimpleDesktops.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SimpleDesktops.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SimpleDesktops.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "kingfisher", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/onevcat/Kingfisher", 7 | "state" : { 8 | "revision" : "1a7b5480eb750a8e171654b9b1cb0a2cbeb27a55", 9 | "version" : "7.2.1" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-log", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-log.git", 16 | "state" : { 17 | "revision" : "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", 18 | "version" : "1.4.2" 19 | } 20 | }, 21 | { 22 | "identity" : "swiftsoup", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/scinfu/SwiftSoup.git", 25 | "state" : { 26 | "revision" : "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", 27 | "version" : "2.4.2" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /SimpleDesktops.xcodeproj/xcshareddata/xcschemes/SimpleDesktops.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 71 | 73 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /SimpleDesktops.xcodeproj/xcshareddata/xcschemes/WallpaperWidgetExtension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 60 | 62 | 68 | 69 | 70 | 71 | 75 | 76 | 80 | 81 | 85 | 86 | 87 | 88 | 95 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoujiaxin/simple-desktops/5256df04bf4f7e15ce68424a40d164c978e25500/SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png -------------------------------------------------------------------------------- /SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoujiaxin/simple-desktops/5256df04bf4f7e15ce68424a40d164c978e25500/SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-128@2x.png -------------------------------------------------------------------------------- /SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoujiaxin/simple-desktops/5256df04bf4f7e15ce68424a40d164c978e25500/SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png -------------------------------------------------------------------------------- /SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoujiaxin/simple-desktops/5256df04bf4f7e15ce68424a40d164c978e25500/SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-16@2x.png -------------------------------------------------------------------------------- /SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoujiaxin/simple-desktops/5256df04bf4f7e15ce68424a40d164c978e25500/SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png -------------------------------------------------------------------------------- /SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoujiaxin/simple-desktops/5256df04bf4f7e15ce68424a40d164c978e25500/SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-256@2x.png -------------------------------------------------------------------------------- /SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoujiaxin/simple-desktops/5256df04bf4f7e15ce68424a40d164c978e25500/SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png -------------------------------------------------------------------------------- /SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoujiaxin/simple-desktops/5256df04bf4f7e15ce68424a40d164c978e25500/SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-32@2x.png -------------------------------------------------------------------------------- /SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoujiaxin/simple-desktops/5256df04bf4f7e15ce68424a40d164c978e25500/SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png -------------------------------------------------------------------------------- /SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoujiaxin/simple-desktops/5256df04bf4f7e15ce68424a40d164c978e25500/SimpleDesktops/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png -------------------------------------------------------------------------------- /SimpleDesktops/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon-16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "AppIcon-16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "AppIcon-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "AppIcon-32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "AppIcon-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "AppIcon-128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "AppIcon-256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "AppIcon-256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "AppIcon-512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "AppIcon-512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SimpleDesktops/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SimpleDesktops/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | LSApplicationCategoryType 22 | public.app-category.utilities 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | LSUIElement 26 | 27 | NSAppTransportSecurity 28 | 29 | NSExceptionDomains 30 | 31 | simpledesktops.com 32 | 33 | NSIncludesSubdomains 34 | 35 | NSTemporaryExceptionAllowsInsecureHTTPLoads 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /SimpleDesktops/Models/ChangeInterval.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChangeInterval.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/3/8. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ChangeInterval: String, CaseIterable, Identifiable, Codable { 11 | case whenWakingFromSleep = "When waking from sleep" 12 | case everyMinute = "Every minute" 13 | case everyFiveMinutes = "Every 5 minutes" 14 | case everyFifteenMinutes = "Every 15 minutes" 15 | case everyThirtyMinutes = "Every 30 minutes" 16 | case everyHour = "Every hour" 17 | case everyDay = "Every day" 18 | 19 | var id: String { 20 | return rawValue 21 | } 22 | 23 | var seconds: TimeInterval? { 24 | switch self { 25 | case .whenWakingFromSleep: return nil 26 | case .everyMinute: return 60 27 | case .everyFiveMinutes: return 5 * 60 28 | case .everyFifteenMinutes: return 15 * 60 29 | case .everyThirtyMinutes: return 30 * 60 30 | case .everyHour: return 60 * 60 31 | case .everyDay: return 24 * 60 * 60 32 | } 33 | } 34 | 35 | static var timeChangeIntervals: [Self] { 36 | allCases.filter { $0.seconds != nil } 37 | } 38 | 39 | static var eventChangeIntervals: [Self] { 40 | allCases.filter { $0.seconds == nil } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SimpleDesktops/Models/Options.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Options.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/1/16. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | struct Options: Codable { 12 | /// Whether to change wallpaper automatically, default is true. 13 | var autoChange: Bool = true 14 | 15 | /// Time interval for automatic wallpaper change. 16 | var changeInterval: ChangeInterval = .everyHour 17 | 18 | // TODO: Launch when log in 19 | 20 | private static let logger = Logger(for: Self.self) 21 | 22 | /// Load options from [UserDefaults](https://developer.apple.com/documentation/foundation/preferences). If there is no data, use the default value. 23 | /// - Parameter userDefaults: UserDefaults object, default is [UserDefaults.standard](https://developer.apple.com/documentation/foundation/userdefaults). 24 | init(from userDefaults: UserDefaults = .standard) { 25 | let keys = Mirror(reflecting: self).children.compactMap { $0.label } 26 | do { 27 | let data = try JSONSerialization.data( 28 | withJSONObject: userDefaults.dictionaryWithValues(forKeys: keys), 29 | options: .fragmentsAllowed 30 | ) 31 | self = try JSONDecoder().decode(Options.self, from: data) 32 | Self.logger.info("Options loaded") 33 | } catch { 34 | Self.logger.info("No options data found, use default value") 35 | } 36 | } 37 | 38 | /// Save current options to UserDefaults. 39 | /// - Parameter userDefaults: UserDefaults object, default is [UserDefaults.standard](https://developer.apple.com/documentation/foundation/userdefaults). 40 | func save(to userDefaults: UserDefaults = .standard) { 41 | do { 42 | let data = try JSONEncoder().encode(self) 43 | if let dictionary = try JSONSerialization.jsonObject( 44 | with: data, 45 | options: .allowFragments 46 | ) as? [String: Any] { 47 | userDefaults.setValuesForKeys(dictionary) 48 | Self.logger.info("Options saved") 49 | } 50 | } catch { 51 | Self.logger.error("Failed to save options, \(error.localizedDescription)") 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /SimpleDesktops/Models/SDPictureInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SDPictureInfo.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/10/30. 6 | // 7 | 8 | import CoreData 9 | import Foundation 10 | 11 | struct SDPictureInfo { 12 | let name: String 13 | 14 | let previewURL: URL 15 | 16 | let url: URL 17 | } 18 | -------------------------------------------------------------------------------- /SimpleDesktops/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SimpleDesktops/SimpleDesktops.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(TeamIdentifierPrefix)me.jiaxin.SimpleDesktops 10 | 11 | com.apple.security.files.downloads.read-write 12 | 13 | com.apple.security.network.client 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /SimpleDesktops/SimpleDesktops.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | SimpleDesktops.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /SimpleDesktops/SimpleDesktops.xcdatamodeld/SimpleDesktops.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /SimpleDesktops/SimpleDesktopsApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleDesktopsApp.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/1/14. 6 | // 7 | 8 | import SwiftUI 9 | import UserNotifications 10 | 11 | // MARK: - 12 | 13 | @main 14 | struct SimpleDesktopsApp: App { 15 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 16 | 17 | var body: some Scene { 18 | WindowGroup {} 19 | } 20 | } 21 | 22 | // MARK: - 23 | 24 | class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { 25 | private var statusItem: NSStatusItem! 26 | private var popover: NSPopover! 27 | 28 | func applicationDidFinishLaunching(_: Notification) { 29 | // No window 30 | NSApp.windows.forEach { $0.close() } 31 | 32 | let viewContext = PersistenceController.shared.container.viewContext 33 | 34 | statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) 35 | statusItem.button?.image = NSImage( 36 | systemSymbolName: "photo.on.rectangle", 37 | accessibilityDescription: nil 38 | ) 39 | statusItem.button?.action = #selector(togglePopover(_:)) 40 | 41 | popover = NSPopover() 42 | popover.behavior = NSPopover.Behavior.transient 43 | popover.contentViewController = NSHostingController(rootView: 44 | PopoverView() 45 | .environment(\.managedObjectContext, viewContext) 46 | .environmentObject(PictureService(context: viewContext))) 47 | 48 | // Start the timer 49 | let options = Options() 50 | WallpaperManager.shared.autoChangeInterval = options.autoChange ? options 51 | .changeInterval : nil 52 | 53 | UNUserNotificationCenter.current().delegate = self 54 | } 55 | 56 | func application(_: NSApplication, open _: [URL]) { 57 | togglePopover(self) 58 | } 59 | 60 | @objc private func togglePopover(_ sender: Any?) { 61 | if popover.isShown { 62 | popover.performClose(sender) 63 | } else { 64 | popover.show( 65 | relativeTo: statusItem.button!.bounds, 66 | of: statusItem.button!, 67 | preferredEdge: NSRectEdge.minY 68 | ) 69 | popover.contentViewController?.view.window?.makeKey() 70 | } 71 | } 72 | 73 | // MARK: - UNUserNotificationCenterDelegate 74 | 75 | func userNotificationCenter( 76 | _: UNUserNotificationCenter, 77 | willPresent _: UNNotification, 78 | withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) 79 | -> Void 80 | ) { 81 | // Display user notification even while the app is in foreground 82 | completionHandler([.banner]) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /SimpleDesktops/SimpleDesktopsRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleDesktopsRequest.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/2/8. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | import SwiftSoup 11 | 12 | struct SimpleDesktopsRequest { 13 | enum RequestError: Error { 14 | case badRequest 15 | case parseFailed 16 | } 17 | 18 | /// The shared singleton session object. 19 | static let shared: SimpleDesktopsRequest = { 20 | let request = SimpleDesktopsRequest(session: .shared) 21 | request.updateMaxPageNumber() 22 | return request 23 | }() 24 | 25 | /// Create an instance. It is recommended to use only in unit tests. 26 | /// - Parameter session: [URLSession](https://developer.apple.com/documentation/foundation/url_loading_system) used to send request. 27 | init(session: URLSession) { 28 | self.session = session 29 | } 30 | 31 | /// Fetch information about a random picture. 32 | /// - Returns: Picture information. 33 | func random() async throws -> SDPictureInfo { 34 | let page = Int.random(in: 1 ... maxPageNumber) 35 | let url = Self.baseURL.appendingPathComponent(String(page)) 36 | 37 | let (data, response) = try await session.data(from: url) 38 | guard (response as? HTTPURLResponse)?.statusCode == 200 else { 39 | logger.error("Bad request") 40 | throw RequestError.badRequest 41 | } 42 | 43 | let info = try String(data: data, encoding: .utf8) 44 | .map { try SwiftSoup.parse($0).select("img") }? 45 | .map { try $0.attr("src") } 46 | .randomElement() 47 | .flatMap(SDPictureInfo.init) 48 | 49 | guard let info = info else { 50 | throw RequestError.parseFailed 51 | } 52 | logger.info("Picture info fetched from Simple Desktops: \(String(describing: info))") 53 | 54 | return info 55 | } 56 | 57 | // MARK: - Private members 58 | 59 | /// [URLSession](https://developer.apple.com/documentation/foundation/url_loading_system) used to send request. 60 | private let session: URLSession 61 | 62 | private let logger = Logger(for: Self.self) 63 | 64 | /// Max page number saved in UserDefaults. If not found, use the default value. 65 | private var maxPageNumber: Int { 66 | let value = UserDefaults.standard.integer(forKey: Self.maxPageNumberKey) 67 | return value > 0 ? value : Self.defaultMaxPageNumber 68 | } 69 | 70 | /// Update the max page number in background task. 71 | private func updateMaxPageNumber() { 72 | let currentValue = maxPageNumber 73 | let url = Self.baseURL.appendingPathComponent(String(currentValue)) 74 | Task(priority: .background) { 75 | do { 76 | let (data, _) = try await session.data(from: url) 77 | let numberOfImgTags = try String(data: data, encoding: .utf8) 78 | .map { try SwiftSoup.parse($0).select("img") }?.count ?? 0 79 | if numberOfImgTags > 0 { 80 | let newValue = currentValue + 1 81 | UserDefaults.standard.setValue(newValue, forKey: Self.maxPageNumberKey) 82 | logger.info("Max page number updated: \(newValue)") 83 | } else { 84 | logger.info("Max page number is already up to date: \(currentValue)") 85 | } 86 | } catch { 87 | logger.error("Failed to update max page number, \(error.localizedDescription)") 88 | } 89 | } 90 | } 91 | 92 | // MARK: - Constants 93 | 94 | private static let baseURL = URL(string: "http://simpledesktops.com/browse")! 95 | private static let maxPageNumberKey = "sdMaxPageNumber" 96 | private static let defaultMaxPageNumber = 52 97 | } 98 | 99 | extension SDPictureInfo { 100 | init?(from link: String) { 101 | let previewURL = URL(string: link) 102 | 103 | let url = previewURL.map { url -> URL in 104 | let lastPathComponent = url.lastPathComponent.split( 105 | separator: ".", 106 | omittingEmptySubsequences: false 107 | )[..<2].joined(separator: ".") 108 | return url.deletingLastPathComponent().appendingPathComponent(lastPathComponent) 109 | } 110 | 111 | let name = url?.pathComponents.split(separator: "desktops").last?.joined(separator: "-") 112 | 113 | guard let name = name, 114 | let previewURL = previewURL, 115 | let url = url 116 | else { 117 | return nil 118 | } 119 | 120 | self.name = name 121 | self.previewURL = previewURL 122 | self.url = url 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /SimpleDesktops/Utilities/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/10/30. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | extension Logger { 12 | init(for type: T.Type) { 13 | self.init(label: "\(Bundle.main.bundleIdentifier!).\(type)") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SimpleDesktops/Utilities/PersistenceController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistenceController.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/1/14. 6 | // 7 | 8 | import CoreData 9 | 10 | struct PersistenceController { 11 | static let shared = PersistenceController() 12 | 13 | static let preview: PersistenceController = { 14 | let result = PersistenceController(inMemory: true) 15 | let viewContext = result.container.viewContext 16 | 17 | let info = 18 | SDPictureInfo( 19 | from: "http://static.simpledesktops.com/uploads/desktops/2020/06/28/Big_Sur_Simple.png.625x385_q100.png" 20 | )! 21 | Picture(context: viewContext).update(with: info) 22 | 23 | return result 24 | }() 25 | 26 | let container: NSPersistentContainer 27 | 28 | private init(inMemory: Bool = false) { 29 | container = NSPersistentContainer(name: "SimpleDesktops") 30 | if inMemory { 31 | container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") 32 | } 33 | container.loadPersistentStores(completionHandler: { _, error in 34 | if let error = error as NSError? { 35 | // Replace this implementation with code to handle the error appropriately. 36 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 37 | 38 | /* 39 | Typical reasons for an error here include: 40 | * The parent directory does not exist, cannot be created, or disallows writing. 41 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 42 | * The device is out of space. 43 | * The store could not be migrated to the current model version. 44 | Check the error message to determine what the actual problem was. 45 | */ 46 | fatalError("Unresolved error \(error), \(error.userInfo)") 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SimpleDesktops/Utilities/UserNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserNotification.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/2/23. 6 | // 7 | 8 | import Kingfisher 9 | import UserNotifications 10 | 11 | struct UserNotification { 12 | static func request(title: String, body: String, attachmentURLs: [URL?] = []) async throws { 13 | // Request authorization 14 | guard try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) 15 | else { 16 | return 17 | } 18 | 19 | let content = UNMutableNotificationContent() 20 | content.title = NSString.localizedUserNotificationString(forKey: title, arguments: nil) 21 | content.body = NSString.localizedUserNotificationString(forKey: body, arguments: nil) 22 | content.attachments = try attachmentURLs.compactMap { url in 23 | guard let url = url else { 24 | return nil 25 | } 26 | 27 | // Copy attachment files to temporary directory 28 | let attachmentURL = FileManager.default.temporaryDirectory 29 | .appendingPathComponent(url.lastPathComponent) 30 | 31 | // Retrieve image data from cache 32 | KingfisherManager.shared.cache.retrieveImage(forKey: url.absoluteString) { result in 33 | if case let .success(imageResult) = result { 34 | try? imageResult.image?.tiffRepresentation?.write(to: attachmentURL) 35 | } 36 | } 37 | 38 | return try UNNotificationAttachment( 39 | identifier: url.lastPathComponent, 40 | url: attachmentURL, 41 | options: nil 42 | ) 43 | } 44 | 45 | let request = UNNotificationRequest( 46 | identifier: UUID().uuidString, 47 | content: content, 48 | trigger: nil 49 | ) 50 | 51 | try await UNUserNotificationCenter.current().add(request) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SimpleDesktops/Utilities/WallpaperManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WallpaperManager.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/1/17. 6 | // 7 | 8 | import AppKit 9 | import Combine 10 | 11 | class WallpaperManager { 12 | static let shared = WallpaperManager() 13 | 14 | /// The directory where wallpaper images are stored. 15 | static let directory: URL = { 16 | let url = FileManager.default 17 | .containerURL( 18 | forSecurityApplicationGroupIdentifier: "8TA5C5ASM9.me.jiaxin.SimpleDesktops" 19 | )! 20 | .appendingPathComponent("Wallpapers") 21 | 22 | // Create the directory if it does not exist 23 | if !FileManager.default.fileExists(atPath: url.path) { 24 | try? FileManager.default.createDirectory( 25 | at: url, 26 | withIntermediateDirectories: true, 27 | attributes: nil 28 | ) 29 | } 30 | 31 | return url 32 | }() 33 | 34 | var autoChangePublisher: AnyPublisher 35 | 36 | /// Time interval for automatic wallpaper change. Set to `nil` to stop. 37 | var autoChangeInterval: ChangeInterval? { 38 | willSet { 39 | timerCancellable?.cancel() 40 | wakeFromSleepObserver.map { 41 | NSWorkspace.shared.notificationCenter.removeObserver($0) 42 | } 43 | guard let interval = newValue else { 44 | return 45 | } 46 | 47 | switch interval { 48 | case .whenWakingFromSleep: 49 | let subject = PassthroughSubject() 50 | wakeFromSleepObserver = NSWorkspace.shared.notificationCenter.addObserver( 51 | forName: NSWorkspace.didWakeNotification, 52 | object: nil, 53 | queue: nil 54 | ) { _ in 55 | subject.send(Date()) 56 | } 57 | autoChangePublisher = subject.eraseToAnyPublisher() 58 | case .everyMinute, 59 | .everyFiveMinutes, 60 | .everyFifteenMinutes, 61 | .everyThirtyMinutes, 62 | .everyHour, 63 | .everyDay: 64 | let publihser = Timer.publish(every: interval.seconds!, on: .main, in: .common) 65 | autoChangePublisher = publihser.eraseToAnyPublisher() 66 | timerCancellable = publihser.connect() 67 | } 68 | } 69 | } 70 | 71 | /// Set wallpaper for all Spaces. 72 | /// - Parameter url: A file URL to the image. 73 | func setWallpaper(with url: URL) { 74 | // TODO: Log 75 | NSScreen.screens.forEach { screen in 76 | try? NSWorkspace.shared.setDesktopImageURL(url, for: screen, options: [:]) 77 | } 78 | 79 | // Multi workspace 80 | workspaceChangeObserver.map { 81 | NSWorkspace.shared.notificationCenter.removeObserver($0) 82 | } 83 | workspaceChangeObserver = NSWorkspace.shared.notificationCenter.addObserver( 84 | forName: NSWorkspace.activeSpaceDidChangeNotification, 85 | object: nil, 86 | queue: nil 87 | ) { _ in 88 | // Set wallpaper when Spaces changed 89 | NSScreen.screens.forEach { screen in 90 | try? NSWorkspace.shared.setDesktopImageURL(url, for: screen, options: [:]) 91 | } 92 | } 93 | } 94 | 95 | private var timerCancellable: Cancellable? 96 | 97 | private var wakeFromSleepObserver: NSObjectProtocol? 98 | 99 | private var workspaceChangeObserver: NSObjectProtocol? 100 | 101 | private init() { 102 | autoChangePublisher = Timer.publish(every: .infinity, on: .main, in: .common) 103 | .eraseToAnyPublisher() 104 | } 105 | 106 | deinit { 107 | wakeFromSleepObserver.map { 108 | NSWorkspace.shared.notificationCenter.removeObserver($0) 109 | } 110 | 111 | workspaceChangeObserver.map { 112 | NSWorkspace.shared.notificationCenter.removeObserver($0) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /SimpleDesktops/ViewModels/Picture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Picture.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/2/7. 6 | // 7 | 8 | import CoreData 9 | 10 | extension Picture { 11 | /// Construct a description of search criteria used to retrieve pictures from Core Data. 12 | /// - Parameters: 13 | /// - predicate: The predicate of the fetch request. 14 | /// - fetchLimit: The fetch limit of the fetch request. 15 | /// - Returns: An instance of [NSFetchRequest](https://developer.apple.com/documentation/coredata/nsfetchrequest). 16 | static func fetchRequest(_ predicate: NSPredicate?, 17 | fetchLimit: Int = 0) -> NSFetchRequest { 18 | let request = fetchRequest() 19 | request.predicate = predicate 20 | request 21 | .sortDescriptors = 22 | [NSSortDescriptor(keyPath: \Picture.lastFetchedTime_, ascending: false)] 23 | request.fetchLimit = fetchLimit 24 | return request 25 | } 26 | 27 | /// Retrieve the first matched picture with the specified value of the key path. 28 | /// - Returns: Retrieved object or `nil` if it does not exist. 29 | static func retrieveFirst(with value: CVarArg, for keyPath: KeyPath, 30 | in context: NSManagedObjectContext) -> Picture? { 31 | let request = fetchRequest( 32 | NSPredicate(format: "%K == %@", NSExpression(forKeyPath: keyPath).keyPath, value), 33 | fetchLimit: 1 34 | ) 35 | return try? context.fetch(request).first 36 | } 37 | 38 | /// Update the picture with the given SDPictureInfo. 39 | /// - Parameter info: Information of the picture. 40 | func update(with info: SDPictureInfo) { 41 | if id_ == nil { 42 | id_ = UUID() 43 | url_ = info.url 44 | } 45 | lastFetchedTime = Date() 46 | name = info.name 47 | previewURL = info.previewURL 48 | 49 | try? managedObjectContext?.save() 50 | } 51 | 52 | // MARK: - Wrappers for none-optional properties 53 | 54 | public var id: UUID { 55 | get { id_! } 56 | set { id_ = newValue } 57 | } 58 | 59 | var lastFetchedTime: Date { 60 | get { lastFetchedTime_! } 61 | set { lastFetchedTime_ = newValue } 62 | } 63 | 64 | var url: URL { 65 | get { url_! } 66 | set { url_ = newValue } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SimpleDesktops/ViewModels/PictureService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PictureService.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/10/31. 6 | // 7 | 8 | import AppKit 9 | import Kingfisher 10 | import Logging 11 | import SwiftUI 12 | 13 | class PictureService: ObservableObject { 14 | @Published private(set) var isFetching: Bool = false { 15 | willSet { 16 | if isFetching != newValue { 17 | fetchingProgress = 0 18 | } 19 | } 20 | } 21 | 22 | @Published private(set) var fetchingProgress: Double = 0 23 | 24 | @Published private(set) var isDownloading: Bool = false { 25 | willSet { 26 | if isDownloading != newValue { 27 | downloadingProgress = 0 28 | } 29 | } 30 | } 31 | 32 | @Published private(set) var downloadingProgress: Double = 0 33 | 34 | private let context: NSManagedObjectContext 35 | 36 | private let logger = Logger(for: PictureService.self) 37 | 38 | init(context: NSManagedObjectContext) { 39 | self.context = context 40 | 41 | // Launch the app for the first time 42 | if let pictures = try? context.fetch(Picture.fetchRequest(nil)), pictures.isEmpty { 43 | Task { 44 | await fetch() 45 | } 46 | } 47 | } 48 | 49 | @MainActor func fetch() async { 50 | isFetching = true 51 | defer { isFetching = false } 52 | 53 | do { 54 | let info = try await SimpleDesktopsRequest.shared.random() 55 | fetchingProgress = 0.3 56 | 57 | let (bytes, response) = try await URLSession.shared.bytes(from: info.previewURL) 58 | let length = Int(response.expectedContentLength) 59 | var data = Data(capacity: length) 60 | for try await byte in bytes { 61 | data.append(byte) 62 | fetchingProgress = 0.3 + 0.7 * Double(data.count) / Double(length) 63 | } 64 | 65 | KingfisherManager.shared.cache.store( 66 | NSImage(data: data)!, 67 | forKey: info.previewURL.absoluteString 68 | ) 69 | 70 | withAnimation(.easeInOut) { 71 | let picture = Picture.retrieveFirst( 72 | with: info.url.absoluteString, 73 | for: \.url_, 74 | in: context 75 | ) ?? Picture(context: context) 76 | picture.update(with: info) 77 | } 78 | } catch { 79 | logger.error("Failed to fetch picture, \(error.localizedDescription)") 80 | } 81 | } 82 | 83 | func download(_ picture: Picture, 84 | to destination: URL = try! FileManager.default.url( 85 | for: .downloadsDirectory, 86 | in: .userDomainMask, 87 | appropriateFor: nil, 88 | create: false 89 | ), 90 | completed: ((URL) async -> Void)? = nil) { 91 | isDownloading = true 92 | 93 | let url = destination.appendingPathComponent(picture.name ?? picture.id.uuidString) 94 | guard !FileManager.default.fileExists(atPath: url.path) else { 95 | isDownloading = false 96 | Task { 97 | await completed?(url) 98 | } 99 | logger.info("Picture is already downloaded") 100 | return 101 | } 102 | 103 | KingfisherManager.shared.downloader 104 | .downloadImage(with: picture.url, options: nil) { [weak self] receivedSize, totalSize in 105 | self?.downloadingProgress = Double(receivedSize) / Double(totalSize) 106 | } completionHandler: { [weak self] result in 107 | self?.isDownloading = false 108 | switch result { 109 | case let .failure(error): 110 | self?.logger.error("Failed to download picture, \(error.localizedDescription)") 111 | case let .success(imageResult): 112 | guard let data = imageResult.image.tiffRepresentation else { 113 | return 114 | } 115 | 116 | do { 117 | try data.write(to: url) 118 | Task { 119 | await completed?(url) 120 | } 121 | self?.logger.info("Picture downloaded to \(url.path)") 122 | } catch { 123 | self?.logger.error("Failed to save picture, \(error.localizedDescription)") 124 | } 125 | } 126 | } 127 | } 128 | 129 | func setWallpaper(_ picture: Picture) { 130 | download(picture, to: WallpaperManager.directory) { url in 131 | WallpaperManager.shared.setWallpaper(with: url) 132 | try? await UserNotification.request( 133 | title: "Wallpaper Changed", 134 | body: url.lastPathComponent, 135 | attachmentURLs: [picture.previewURL] 136 | ) 137 | } 138 | } 139 | 140 | func cancelDownload() { 141 | KingfisherManager.shared.downloader.cancelAll() 142 | } 143 | 144 | func delete(_ picture: Picture) { 145 | do { 146 | try withAnimation(.easeInOut) { 147 | context.delete(picture) 148 | try context.save() 149 | } 150 | 151 | logger.info("Picture deleted, \(picture)") 152 | } catch { 153 | logger.error("Failed to delete picture, \(error.localizedDescription)") 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /SimpleDesktops/ViewModels/Preferences.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Preferences.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/1/16. 6 | // 7 | 8 | import Foundation 9 | 10 | class Preferences: ObservableObject { 11 | @Published var autoChange: Bool { 12 | willSet { 13 | options.autoChange = newValue 14 | options.save() 15 | WallpaperManager.shared.autoChangeInterval = newValue ? options.changeInterval : nil 16 | } 17 | } 18 | 19 | @Published var changeInterval: ChangeInterval { 20 | willSet { 21 | options.changeInterval = newValue 22 | options.save() 23 | WallpaperManager.shared.autoChangeInterval = options.autoChange ? newValue : nil 24 | } 25 | } 26 | 27 | private var options = Options() 28 | 29 | init() { 30 | autoChange = options.autoChange 31 | changeInterval = options.changeInterval 32 | 33 | if options.autoChange { 34 | WallpaperManager.shared.autoChangeInterval = options.changeInterval 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SimpleDesktops/Views/HistoryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryView.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/1/15. 6 | // 7 | 8 | import Kingfisher 9 | import SwiftUI 10 | 11 | struct HistoryView: View { 12 | @EnvironmentObject private var service: PictureService 13 | 14 | @State private var hoveringItem: Picture? 15 | 16 | let pictures: [Picture] 17 | 18 | var body: some View { 19 | ScrollView { 20 | Spacer(minLength: highlighStrokeWidth) 21 | 22 | LazyVGrid(columns: Array(repeating: GridItem(.fixed(pictureWidth), 23 | spacing: pictureSpacing), count: 2)) { 24 | ForEach(pictures) { picture in 25 | KFImage(picture.previewURL) 26 | .resizable() 27 | .aspectRatio(pictureAspectRatio, contentMode: .fit) 28 | .border( 29 | Color.accentColor, 30 | width: hoveringItem == picture ? highlighStrokeWidth : 0 31 | ) 32 | .onHover { isHovering in 33 | hoveringItem = isHovering ? picture : nil 34 | } 35 | .contextMenu { 36 | // Download button 37 | Button { 38 | service.download(picture) { url in 39 | try? await UserNotification.request( 40 | title: "Picture Downloaded", 41 | body: url.lastPathComponent, 42 | attachmentURLs: [picture.previewURL] 43 | ) 44 | } 45 | } label: { 46 | Text("Download") 47 | } 48 | .keyboardShortcut("d") 49 | 50 | // Set wallpaper button 51 | Button { 52 | service.setWallpaper(picture) 53 | } label: { 54 | Text("Set as wallpaper") 55 | } 56 | 57 | Divider() 58 | 59 | // Delete button 60 | Button { 61 | service.delete(picture) 62 | } label: { 63 | Text("Delete") 64 | } 65 | .keyboardShortcut(.delete) 66 | } 67 | } 68 | } 69 | 70 | Spacer(minLength: highlighStrokeWidth) 71 | } 72 | } 73 | 74 | // MARK: - Constants 75 | 76 | private let highlighStrokeWidth: CGFloat = 3 77 | private let pictureAspectRatio: CGFloat = 1.6 78 | private let pictureWidth: CGFloat = 176 79 | private let pictureSpacing: CGFloat = 16 80 | } 81 | 82 | struct HistoryView_Previews: PreviewProvider { 83 | static var previews: some View { 84 | let viewContext = PersistenceController.preview.container.viewContext 85 | let pictures = try! viewContext.fetch(Picture.fetchRequest(nil)) 86 | HistoryView(pictures: pictures) 87 | .environmentObject(PictureService(context: viewContext)) 88 | .frame(width: 400, height: 314) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /SimpleDesktops/Views/Modifiers/CapsuledButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CapsuledButtonStyle.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/2/8. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CapsuledButtonStyle: ButtonStyle { 11 | let size: CGSize 12 | 13 | let strokeWidth: CGFloat 14 | 15 | init(size: CGSize = CGSize(width: 240, height: 40), strokeWidth: CGFloat = 2.0) { 16 | self.size = size 17 | self.strokeWidth = strokeWidth 18 | } 19 | 20 | func makeBody(configuration: Configuration) -> some View { 21 | CapsuledButton(configuration: configuration, size: size, strokeWidth: strokeWidth) 22 | } 23 | 24 | struct CapsuledButton: View { 25 | let configuration: ButtonStyle.Configuration 26 | 27 | let size: CGSize 28 | 29 | let strokeWidth: CGFloat 30 | 31 | @Environment(\.isEnabled) private var isEnabled: Bool 32 | 33 | @State private var isHovering: Bool = false 34 | 35 | var body: some View { 36 | configuration.label 37 | .frame(width: size.width, height: size.height, alignment: .center) 38 | .background( 39 | Capsule() 40 | .stroke(lineWidth: strokeWidth) 41 | ) 42 | .contentShape(Capsule()) 43 | .foregroundColor(isEnabled ? (isHovering ? .accentColor : .primary) : .secondary) 44 | .opacity(configuration.isPressed ? 0.8 : 1.0) 45 | .scaleEffect(configuration.isPressed ? 0.95 : 1.0) 46 | .onHover { isHovering in 47 | self.isHovering = isHovering 48 | } 49 | } 50 | } 51 | } 52 | 53 | struct CapsuledButton_Previews: PreviewProvider { 54 | static var previews: some View { 55 | button 56 | .buttonStyle(CapsuledButtonStyle(size: CGSize(width: 300, height: 60), 57 | strokeWidth: 4.0)) 58 | 59 | button 60 | .buttonStyle(CapsuledButtonStyle(size: CGSize(width: 300, height: 60), 61 | strokeWidth: 4.0)) 62 | .disabled(true) 63 | } 64 | 65 | static var button: some View { 66 | Button {} label: { 67 | Text("CapsuledButton") 68 | .font(.largeTitle) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /SimpleDesktops/Views/Modifiers/ImageButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageButtonStyle.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/2/8. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ImageButtonStyle: ButtonStyle { 11 | let size: CGSize 12 | 13 | init(size: CGSize = CGSize(width: 32, height: 32)) { 14 | self.size = size 15 | } 16 | 17 | func makeBody(configuration: Configuration) -> some View { 18 | ImageButton(configuration: configuration, size: size) 19 | } 20 | 21 | struct ImageButton: View { 22 | let configuration: ButtonStyle.Configuration 23 | 24 | let size: CGSize 25 | 26 | @Environment(\.isEnabled) private var isEnabled: Bool 27 | 28 | @State private var isHovering: Bool = false 29 | 30 | var body: some View { 31 | configuration.label 32 | .frame(width: size.width, height: size.height) 33 | .contentShape(Rectangle()) 34 | .background { 35 | RoundedRectangle(cornerRadius: 6) 36 | .opacity(isHovering && isEnabled ? 0.2 : 0) 37 | .transition(.opacity) 38 | } 39 | .foregroundColor(isEnabled ? .primary : .secondary) 40 | .onHover { isHovering in 41 | withAnimation(.easeInOut) { 42 | self.isHovering = isHovering 43 | } 44 | } 45 | .scaleEffect(configuration.isPressed ? 0.9 : 1.0) 46 | } 47 | } 48 | } 49 | 50 | struct ImageButton_Previews: PreviewProvider { 51 | static var previews: some View { 52 | button 53 | .buttonStyle(ImageButtonStyle(size: CGSize(width: 40, height: 40))) 54 | 55 | button 56 | .buttonStyle(ImageButtonStyle(size: CGSize(width: 40, height: 40))) 57 | .disabled(true) 58 | } 59 | 60 | static var button: some View { 61 | Button {} label: { 62 | Image(systemName: "applelogo") 63 | .font(.largeTitle) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SimpleDesktops/Views/PopoverView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopoverView.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/1/14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PopoverView: View { 11 | enum ViewState { 12 | case preview 13 | case preference 14 | case history 15 | 16 | var height: CGFloat { 17 | switch self { 18 | case .preference: 19 | return smallPopoverHeight 20 | case .preview, 21 | .history: 22 | return largePopoverHeight 23 | } 24 | } 25 | } 26 | 27 | @EnvironmentObject private var service: PictureService 28 | 29 | @FetchRequest(fetchRequest: Picture 30 | .fetchRequest(nil)) private var pictures: FetchedResults 31 | 32 | @State private var currentView: ViewState = .preview 33 | 34 | // MARK: - Views 35 | 36 | var body: some View { 37 | VStack(spacing: 0) { 38 | if currentView != .preference { 39 | navigationBar 40 | } 41 | 42 | content() 43 | } 44 | .frame(width: popoverWidth, height: currentView.height) 45 | .onReceive(WallpaperManager.shared.autoChangePublisher) { _ in 46 | Task { 47 | await service.fetch() 48 | if let picture = pictures.first { 49 | service.setWallpaper(picture) 50 | } 51 | } 52 | } 53 | } 54 | 55 | private var navigationBar: some View { 56 | HStack { 57 | if currentView == .preview { 58 | // Preference button 59 | Button(action: transitToPreferenceView) { 60 | Image(systemName: "gearshape") 61 | .font(Font.system(size: navigationBarButtonIconSize, weight: .bold)) 62 | } 63 | .keyboardShortcut(",", modifiers: .command) 64 | 65 | // History button 66 | Button(action: transitToHistoryView) { 67 | Image(systemName: "clock") 68 | .font(Font.system(size: navigationBarButtonIconSize, weight: .bold)) 69 | } 70 | } else if currentView == .history { 71 | // Back to preview button 72 | Button(action: transitToPreviewView) { 73 | Image(systemName: "chevron.backward") 74 | .font(Font.system(size: navigationBarButtonIconSize, weight: .bold)) 75 | } 76 | } 77 | 78 | Spacer() 79 | 80 | if service.isDownloading { 81 | // Download progress indicator 82 | ProgressView(value: service.downloadingProgress) 83 | .frame(width: downloadProgressIndicatorWidth) 84 | 85 | // Cancel download button 86 | Button(action: service.cancelDownload) { 87 | Image(systemName: "xmark") 88 | .font(Font.system(size: navigationBarButtonIconSize, weight: .bold)) 89 | } 90 | } else if currentView == .preview { 91 | // Delete button 92 | Button { 93 | if let picture = pictures.first { 94 | service.delete(picture) 95 | } 96 | } label: { 97 | Image(systemName: "trash") 98 | .font(Font.system(size: navigationBarButtonIconSize, weight: .bold)) 99 | } 100 | .disabled(service.isFetching) 101 | 102 | // Download button 103 | Button { 104 | if let picture = pictures.first { 105 | service.download(picture) { url in 106 | try? await UserNotification.request( 107 | title: "Picture Downloaded", 108 | body: url.lastPathComponent, 109 | attachmentURLs: [picture.previewURL] 110 | ) 111 | } 112 | } 113 | } label: { 114 | Image(systemName: "square.and.arrow.down") 115 | .font(Font.system(size: navigationBarButtonIconSize, weight: .bold)) 116 | } 117 | .disabled(service.isFetching) 118 | } 119 | } 120 | .buttonStyle(ImageButtonStyle()) 121 | .padding(navigationBarPadding) 122 | } 123 | 124 | @ViewBuilder 125 | private func content() -> some View { 126 | switch currentView { 127 | case .preview: 128 | PreviewView(picture: pictures.first) 129 | 130 | case .preference: 131 | PreferenceView(currentView: $currentView) 132 | .transition(.move(edge: .bottom)) 133 | 134 | case .history: 135 | HistoryView(pictures: pictures.map { $0 }) 136 | .transition(.move(edge: .trailing)) 137 | } 138 | } 139 | 140 | // MARK: - Functions 141 | 142 | private func transitToPreviewView() { 143 | withAnimation(.easeInOut) { 144 | currentView = .preview 145 | } 146 | } 147 | 148 | private func transitToPreferenceView() { 149 | withAnimation(.easeInOut) { 150 | currentView = .preference 151 | } 152 | } 153 | 154 | private func transitToHistoryView() { 155 | withAnimation(.easeInOut) { 156 | currentView = .history 157 | } 158 | } 159 | 160 | // MARK: - Constants 161 | 162 | private static let smallPopoverHeight: CGFloat = 195 163 | private static let largePopoverHeight: CGFloat = 358 164 | private let popoverWidth: CGFloat = 400 165 | private let navigationBarButtonIconSize: CGFloat = 16 166 | private let downloadProgressIndicatorWidth: CGFloat = 60 167 | private let navigationBarPadding: CGFloat = 6 168 | } 169 | 170 | struct PopoverView_Previews: PreviewProvider { 171 | static var previews: some View { 172 | let viewContext = PersistenceController.preview.container.viewContext 173 | PopoverView() 174 | .environment(\.managedObjectContext, viewContext) 175 | .environmentObject(PictureService(context: viewContext)) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /SimpleDesktops/Views/PreferenceView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreferenceView.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/1/15. 6 | // 7 | 8 | import Kingfisher 9 | import SwiftUI 10 | 11 | struct PreferenceView: View { 12 | @Binding var currentView: PopoverView.ViewState 13 | 14 | @StateObject private var preferences = Preferences() 15 | 16 | @State private var cacheSize: Int64 = 0 17 | 18 | var body: some View { 19 | VStack(spacing: contentSpacing) { 20 | HStack { 21 | VStack(alignment: .trailing, spacing: contentSpacing) { 22 | Toggle("Change picture: ", isOn: $preferences.autoChange) 23 | .frame(height: intervalPickerHeight) 24 | 25 | Text("Cache size: ") 26 | .frame(height: intervalPickerHeight) 27 | } 28 | 29 | VStack(alignment: .leading, spacing: contentSpacing) { 30 | Picker("", selection: $preferences.changeInterval) { 31 | ForEach(ChangeInterval.timeChangeIntervals) { interval in 32 | Text(LocalizedStringKey(interval.rawValue)) 33 | .tag(interval) 34 | } 35 | 36 | Divider() 37 | 38 | ForEach(ChangeInterval.eventChangeIntervals) { interval in 39 | Text(LocalizedStringKey(interval.rawValue)) 40 | .tag(interval) 41 | } 42 | } 43 | .frame(width: intervalPickerWidth) 44 | .labelsHidden() 45 | .disabled(!preferences.autoChange) 46 | 47 | HStack { 48 | Text(ByteCountFormatter().string(fromByteCount: cacheSize)) 49 | .frame(height: intervalPickerHeight) 50 | .onAppear(perform: getCacheSize) 51 | 52 | Spacer() 53 | 54 | Button { 55 | KingfisherManager.shared.cache.clearCache(completion: getCacheSize) 56 | } label: { 57 | Text("Clear") 58 | } 59 | } 60 | .frame(width: intervalPickerWidth) 61 | } 62 | } 63 | 64 | Text("Version \(versionNumber) (\(buildNumber))") 65 | .font(.callout) 66 | .foregroundColor(.secondary) 67 | 68 | HStack(spacing: buttonSpacing) { 69 | Button(action: transitToPreview) { 70 | Text("Done") 71 | .fontWeight(.semibold) 72 | } 73 | .buttonStyle(CapsuledButtonStyle(size: CGSize(width: buttonWidth, 74 | height: buttonHeight))) 75 | 76 | Button(action: quit) { 77 | Text("Quit") 78 | .fontWeight(.semibold) 79 | } 80 | .buttonStyle(CapsuledButtonStyle(size: CGSize(width: buttonWidth, 81 | height: buttonHeight))) 82 | } 83 | } 84 | .padding(.vertical, contentVerticalPadding) 85 | } 86 | 87 | // MARK: - Funstions 88 | 89 | private func getCacheSize() { 90 | KingfisherManager.shared.cache.calculateDiskStorageSize { result in 91 | if case let .success(size) = result { 92 | cacheSize = Int64(size) 93 | } 94 | } 95 | } 96 | 97 | private func transitToPreview() { 98 | withAnimation(.easeInOut) { 99 | currentView = .preview 100 | } 101 | } 102 | 103 | private func quit() { 104 | NSApp.terminate(nil) 105 | } 106 | 107 | // MARK: - Constants 108 | 109 | private let intervalPickerWidth: CGFloat = 180 110 | private let intervalPickerHeight: CGFloat = 20 111 | private let buttonSpacing: CGFloat = 24 112 | private let buttonWidth: CGFloat = 120 113 | private let buttonHeight: CGFloat = 40 114 | private let contentSpacing: CGFloat = 20 115 | private let contentVerticalPadding: CGFloat = 20 116 | 117 | private let versionNumber = Bundle.main 118 | .object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String 119 | private let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String 120 | } 121 | 122 | struct PreferenceView_Previews: PreviewProvider { 123 | static var previews: some View { 124 | PreferenceView(currentView: .constant(.preference)) 125 | .frame(width: 400) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /SimpleDesktops/Views/PreviewView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewView.swift 3 | // SimpleDesktops 4 | // 5 | // Created by Jiaxin Shou on 2021/1/15. 6 | // 7 | 8 | import Kingfisher 9 | import SwiftUI 10 | 11 | struct PreviewView: View { 12 | @Environment(\.colorScheme) private var colorScheme: ColorScheme 13 | 14 | @EnvironmentObject private var service: PictureService 15 | 16 | @State private var buttonHovering: Bool = false 17 | 18 | let picture: Picture? 19 | 20 | // MARK: - Views 21 | 22 | var body: some View { 23 | VStack(spacing: 0) { 24 | ZStack { 25 | KFImage(picture?.previewURL) 26 | .onlyFromCache() 27 | .resizable() 28 | .aspectRatio(pictureAspectRatio, contentMode: .fit) 29 | .transition(.opacity) 30 | 31 | if service.isFetching { 32 | ProgressView(value: service.fetchingProgress) 33 | .progressViewStyle(CircularProgressViewStyle()) 34 | } else { 35 | fetchButton 36 | } 37 | } 38 | 39 | Button { 40 | if let picture = picture { 41 | service.setWallpaper(picture) 42 | } 43 | } label: { 44 | Text("Set as Wallpaper") 45 | .fontWeight(.semibold) 46 | } 47 | .buttonStyle(CapsuledButtonStyle()) 48 | .padding(setWallpaperButtonPadding) 49 | .disabled(service.isFetching || service.isDownloading) 50 | } 51 | } 52 | 53 | private var fetchButton: some View { 54 | Button { 55 | Task { 56 | await service.fetch() 57 | } 58 | } label: { 59 | Image(systemName: "arrow.triangle.2.circlepath") 60 | .font(Font.system(size: fetchButtonIconSize, weight: .bold)) 61 | .frame( 62 | width: fetchButtonFrameSize, 63 | height: fetchButtonFrameSize, 64 | alignment: .center 65 | ) 66 | .foregroundColor(colorScheme == .dark ? .black : .white) 67 | .background(RoundedRectangle(cornerRadius: fetchButtonCornerRadius)) 68 | .opacity(buttonHovering ? fetchButtonHoveringOpacity : fetchButtonNormalOpacity) 69 | } 70 | .buttonStyle(PlainButtonStyle()) 71 | .onHover { isHovering in 72 | buttonHovering = isHovering 73 | } 74 | } 75 | 76 | // MARK: - Constants 77 | 78 | private let setWallpaperButtonPadding: CGFloat = 12 79 | private let pictureAspectRatio: CGFloat = 1.6 80 | private let fetchButtonIconSize: CGFloat = 32 81 | private let fetchButtonFrameSize: CGFloat = 48 82 | private let fetchButtonCornerRadius: CGFloat = 8 83 | private let fetchButtonHoveringOpacity: Double = 0.8 84 | private let fetchButtonNormalOpacity: Double = 0.2 85 | } 86 | 87 | struct PreviewView_Previews: PreviewProvider { 88 | static var previews: some View { 89 | let viewContext = PersistenceController.preview.container.viewContext 90 | let picture = try? viewContext.fetch(Picture.fetchRequest(nil, fetchLimit: 1)).first 91 | PreviewView(picture: picture) 92 | .environmentObject(PictureService(context: viewContext)) 93 | .frame(width: 400) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /SimpleDesktops/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Simple Desktops 4 | 5 | Created by Jiaxin Shou on 2021/1/20. 6 | 7 | */ 8 | 9 | // MARK: - Preview view 10 | 11 | "Set as Wallpaper"; 12 | 13 | // MARK: - Preference view 14 | 15 | "Change picture: "; 16 | 17 | "Cache size: "; 18 | 19 | "Clear"; 20 | 21 | "Done"; 22 | 23 | "Quit"; 24 | 25 | "When waking from sleep"; 26 | 27 | "Every minute"; 28 | 29 | "Every 5 minutes"; 30 | 31 | "Every 15 minutes"; 32 | 33 | "Every 30 minutes"; 34 | 35 | "Every hour"; 36 | 37 | "Every day"; 38 | 39 | // MARK: - History view 40 | 41 | "Download"; 42 | 43 | "Set as wallpaper"; 44 | 45 | "Delete"; 46 | 47 | // MARK: - User notification 48 | 49 | "Picture Downloaded"; 50 | 51 | "Wallpaper Changed"; 52 | -------------------------------------------------------------------------------- /SimpleDesktops/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Simple Desktops 4 | 5 | Created by Jiaxin Shou on 2021/1/20. 6 | 7 | */ 8 | 9 | // MARK: - Preview view 10 | 11 | "Set as Wallpaper" = "设为壁纸"; 12 | 13 | // MARK: - Preference view 14 | 15 | "Change picture: " = "更换壁纸:"; 16 | 17 | "Cache size: " = "缓存大小:"; 18 | 19 | "Clear" = "清理"; 20 | 21 | "Done" = "完成"; 22 | 23 | "Quit" = "退出"; 24 | 25 | "When waking from sleep" = "从睡眠中唤醒时"; 26 | 27 | "Every minute" = "每分钟"; 28 | 29 | "Every 5 minutes" = "每5分钟"; 30 | 31 | "Every 15 minutes" = "每15分钟"; 32 | 33 | "Every 30 minutes" = "每30分钟"; 34 | 35 | "Every hour" = "每小时"; 36 | 37 | "Every day" = "每天"; 38 | 39 | // MARK: - History view 40 | 41 | "Download" = "下载"; 42 | 43 | "Set as wallpaper" = "设为壁纸"; 44 | 45 | "Delete" = "删除"; 46 | 47 | // MARK: - User notification 48 | 49 | "Picture Downloaded" = "图片已下载"; 50 | 51 | "Wallpaper Changed" = "壁纸已更换"; 52 | -------------------------------------------------------------------------------- /SimpleDesktopsTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | 18 | 19 | -------------------------------------------------------------------------------- /SimpleDesktopsTests/Mock/MockURLProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockURLProtocol.swift 3 | // SimpleDesktopsTests 4 | // 5 | // Created by Jiaxin Shou on 2021/6/26. 6 | // 7 | 8 | import Foundation 9 | 10 | class MockURLProtocol: URLProtocol { 11 | typealias RequestHandler = (URLRequest) -> (Data?, URLResponse?, Error?) 12 | 13 | static var requestHandler: RequestHandler? 14 | 15 | override class func canInit(with _: URLRequest) -> Bool { 16 | return true 17 | } 18 | 19 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 20 | return request 21 | } 22 | 23 | override func startLoading() { 24 | guard let requestHandler = Self.requestHandler else { 25 | return 26 | } 27 | 28 | let (data, response, error) = requestHandler(request) 29 | 30 | if let error = error { 31 | client?.urlProtocol(self, didFailWithError: error) 32 | return 33 | } 34 | 35 | if let data = data, let response = response { 36 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) 37 | client?.urlProtocol(self, didLoad: data) 38 | } 39 | 40 | client?.urlProtocolDidFinishLoading(self) 41 | } 42 | 43 | override func stopLoading() {} 44 | } 45 | -------------------------------------------------------------------------------- /SimpleDesktopsTests/OptionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionsTests.swift 3 | // SimpleDesktopsTests 4 | // 5 | // Created by Jiaxin Shou on 2021/10/30. 6 | // 7 | 8 | @testable import Simple_Desktops 9 | 10 | import XCTest 11 | 12 | class OptionsTests: XCTestCase { 13 | private var userDefaults: UserDefaults! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | 18 | let identifier = Bundle(for: type(of: self)).bundleIdentifier! 19 | userDefaults = UserDefaults(suiteName: identifier) 20 | userDefaults.removePersistentDomain(forName: identifier) 21 | } 22 | 23 | func testInitWithDefaultValue() throws { 24 | let options = Options(from: userDefaults) 25 | 26 | XCTAssertTrue(options.autoChange) 27 | XCTAssertEqual(options.changeInterval, .everyHour) 28 | } 29 | 30 | func testInitFromUserDefaults() throws { 31 | userDefaults.set(false, forKey: "autoChange") 32 | userDefaults.set(ChangeInterval.whenWakingFromSleep.rawValue, forKey: "changeInterval") 33 | 34 | let options = Options(from: userDefaults) 35 | 36 | XCTAssertFalse(options.autoChange) 37 | XCTAssertEqual(options.changeInterval, .whenWakingFromSleep) 38 | } 39 | 40 | func testSave() throws { 41 | var options = Options(from: userDefaults) 42 | options.autoChange = true 43 | options.changeInterval = .whenWakingFromSleep 44 | 45 | XCTAssertFalse(userDefaults.bool(forKey: "autoChange")) 46 | XCTAssertNil(userDefaults.string(forKey: "changeInterval")) 47 | 48 | options.save(to: userDefaults) 49 | 50 | XCTAssertTrue(userDefaults.bool(forKey: "autoChange")) 51 | XCTAssertEqual( 52 | userDefaults.string(forKey: "changeInterval"), 53 | ChangeInterval.whenWakingFromSleep.rawValue 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /SimpleDesktopsTests/PictureTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PictureTests.swift 3 | // SimpleDesktopsTests 4 | // 5 | // Created by Jiaxin Shou on 2021/9/25. 6 | // 7 | 8 | @testable import Simple_Desktops 9 | 10 | import XCTest 11 | 12 | class PictureTests: XCTestCase { 13 | private let context = PersistenceController.preview.container.viewContext 14 | 15 | func testRetrieveExisting() throws { 16 | let url = 17 | URL( 18 | string: "http://static.simpledesktops.com/uploads/desktops/2020/06/28/Big_Sur_Simple.png" 19 | )! 20 | let picture = Picture.retrieveFirst(with: url.absoluteString, for: \.url_, in: context) 21 | 22 | XCTAssertNotNil(picture?.id_) 23 | XCTAssertNotNil(picture?.lastFetchedTime_) 24 | XCTAssertEqual(picture?.name, "2020-06-28-Big_Sur_Simple.png") 25 | XCTAssertEqual( 26 | picture?.previewURL, 27 | URL( 28 | string: "http://static.simpledesktops.com/uploads/desktops/2020/06/28/Big_Sur_Simple.png.625x385_q100.png" 29 | ) 30 | ) 31 | XCTAssertEqual(picture?.url, url) 32 | } 33 | 34 | func testRetrieveNonExistent() throws { 35 | let url = 36 | URL( 37 | string: "http://static.simpledesktops.com/uploads/desktops/2021/02/04/mirage.png.295x184_q100.png" 38 | )! 39 | let picture = Picture.retrieveFirst(with: url.absoluteString, for: \.url_, in: context) 40 | 41 | XCTAssertNil(picture) 42 | } 43 | 44 | func testUpdate() throws { 45 | let link = 46 | "http://static.simpledesktops.com/uploads/desktops/2020/06/28/Big_Sur_Simple.png.625x385_q100.png" 47 | let info = SDPictureInfo(from: link)! 48 | let picture = Picture.retrieveFirst( 49 | with: info.url.absoluteString, 50 | for: \.url_, 51 | in: context 52 | ) 53 | 54 | XCTAssertNotNil(picture?.id_) 55 | XCTAssertNotNil(picture?.lastFetchedTime_) 56 | XCTAssertEqual(picture?.name, info.name) 57 | XCTAssertEqual(picture?.previewURL, info.previewURL) 58 | XCTAssertEqual(picture?.url, info.url) 59 | 60 | let id = picture?.id 61 | let lastFetchedTime = picture?.lastFetchedTime 62 | 63 | picture? 64 | .update(with: SDPictureInfo(name: UUID().uuidString, previewURL: info.url, 65 | url: info.url)) 66 | 67 | XCTAssertEqual(picture?.id, id) 68 | XCTAssertNotEqual(picture?.lastFetchedTime, lastFetchedTime) 69 | XCTAssertNotEqual(picture?.name, info.name) 70 | XCTAssertEqual(picture?.previewURL, info.url) 71 | XCTAssertEqual(picture?.url, info.url) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /SimpleDesktopsTests/SimpleDesktopsRequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleDesktopsRequestTests.swift 3 | // SimpleDesktopsTests 4 | // 5 | // Created by Jiaxin Shou on 2021/6/24. 6 | // 7 | 8 | @testable import Simple_Desktops 9 | 10 | import XCTest 11 | 12 | class SimpleDesktopsRequestTests: XCTestCase { 13 | private var request: SimpleDesktopsRequest! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | 18 | let configuration = URLSessionConfiguration.default 19 | configuration.protocolClasses = [MockURLProtocol.self] 20 | request = SimpleDesktopsRequest(session: URLSession(configuration: configuration)) 21 | } 22 | 23 | func testRandom() async throws { 24 | MockURLProtocol.requestHandler = { _ in 25 | let data = try! Data(contentsOf: Bundle(for: type(of: self)) 26 | .url(forResource: "SimpleDesktopsRequestTests", withExtension: "html")!) 27 | let response = HTTPURLResponse( 28 | url: URL(string: "http://simpledesktops.com/browse/")!, 29 | statusCode: 200, 30 | httpVersion: "HTTP/1.1", 31 | headerFields: nil 32 | ) 33 | return (data, response, nil) 34 | } 35 | 36 | let info = try await request.random() 37 | XCTAssertEqual(info.name, "2021-02-04-mirage.png") 38 | XCTAssertEqual( 39 | info.previewURL, 40 | URL( 41 | string: "http://static.simpledesktops.com/uploads/desktops/2021/02/04/mirage.png.295x184_q100.png" 42 | )! 43 | ) 44 | XCTAssertEqual( 45 | info.url, 46 | URL(string: "http://static.simpledesktops.com/uploads/desktops/2021/02/04/mirage.png")! 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /SimpleDesktopsTests/TestData/SimpleDesktopsRequestTests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 |
12 | 13 | mirage 20 | 21 |

mirage

22 | By: lucy 23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /WallpaperWidget/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WallpaperWidget/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /WallpaperWidget/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /WallpaperWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WallpaperWidget/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | LSMinimumSystemVersion 22 | $(MACOSX_DEPLOYMENT_TARGET) 23 | NSExtension 24 | 25 | NSExtensionPointIdentifier 26 | com.apple.widgetkit-extension 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /WallpaperWidget/WallpaperWidget.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(TeamIdentifierPrefix)me.jiaxin.SimpleDesktops 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /WallpaperWidget/WallpaperWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WallpaperWidget.swift 3 | // WallpaperWidget 4 | // 5 | // Created by Jiaxin Shou on 2021/2/9. 6 | // 7 | 8 | import SwiftUI 9 | import WidgetKit 10 | 11 | struct Provider: TimelineProvider { 12 | func placeholder(in _: Context) -> SimpleEntry { 13 | SimpleEntry(date: Date(), url: nil) 14 | } 15 | 16 | func getSnapshot(in _: Context, completion: @escaping (SimpleEntry) -> Void) { 17 | let entry = SimpleEntry(date: Date(), 18 | url: try? FileManager.default.contentsOfDirectory( 19 | at: WallpaperManager.directory, 20 | includingPropertiesForKeys: nil, 21 | options: .skipsHiddenFiles 22 | ).randomElement()) 23 | completion(entry) 24 | } 25 | 26 | func getTimeline(in _: Context, completion: @escaping (Timeline) -> Void) { 27 | var entries: [SimpleEntry] = [] 28 | 29 | let fileURLs = try! FileManager.default.contentsOfDirectory( 30 | at: WallpaperManager.directory, 31 | includingPropertiesForKeys: nil, 32 | options: .skipsHiddenFiles 33 | ).shuffled() 34 | 35 | let currentDate = Date() 36 | for (index, url) in fileURLs.enumerated() { 37 | let entryDate = Calendar.current.date( 38 | byAdding: .minute, 39 | value: 15 * index, 40 | to: currentDate 41 | )! 42 | let entry = SimpleEntry(date: entryDate, url: url) 43 | entries.append(entry) 44 | } 45 | 46 | let timeline = Timeline(entries: entries, policy: .atEnd) 47 | completion(timeline) 48 | } 49 | } 50 | 51 | struct SimpleEntry: TimelineEntry { 52 | let date: Date 53 | 54 | let url: URL? 55 | } 56 | 57 | struct WallpaperWidgetEntryView: View { 58 | var entry: Provider.Entry 59 | 60 | var body: some View { 61 | if let url = entry.url, 62 | let image = NSImage(contentsOf: url) { 63 | Image(nsImage: image) 64 | .resizable() 65 | .aspectRatio(contentMode: .fill) 66 | } else { 67 | Image(systemName: "photo.on.rectangle.angled") 68 | .font(Font.system(size: 64)) 69 | .foregroundColor(.secondary) 70 | } 71 | } 72 | } 73 | 74 | @main 75 | struct WallpaperWidget: Widget { 76 | let kind: String = "WallpaperWidget" 77 | 78 | var body: some WidgetConfiguration { 79 | StaticConfiguration(kind: kind, provider: Provider()) { entry in 80 | WallpaperWidgetEntryView(entry: entry) 81 | } 82 | .configurationDisplayName("Wallpapers") 83 | .description("View all history wallpapers.") 84 | } 85 | } 86 | 87 | struct WallpaperWidget_Previews: PreviewProvider { 88 | static var previews: some View { 89 | let emptyEntry = SimpleEntry(date: Date(), url: nil) 90 | WallpaperWidgetEntryView(entry: emptyEntry) 91 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 92 | 93 | let demoEntry = SimpleEntry(date: Date(), 94 | url: try? FileManager.default.contentsOfDirectory( 95 | at: WallpaperManager.directory, 96 | includingPropertiesForKeys: nil, 97 | options: .skipsHiddenFiles 98 | ).randomElement()) 99 | WallpaperWidgetEntryView(entry: demoEntry) 100 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /fastlane/Appfile.swift: -------------------------------------------------------------------------------- 1 | var appIdentifier: String { return "[[APP_IDENTIFIER]]" } // The bundle identifier of your app 2 | var appleID: String { return "[[APPLE_ID]]" } // Your Apple email address 3 | 4 | // For more information about the Appfile, see: 5 | // https://docs.fastlane.tools/advanced/#appfile 6 | -------------------------------------------------------------------------------- /fastlane/Fastfile.swift: -------------------------------------------------------------------------------- 1 | // This file contains the fastlane.tools configuration 2 | // You can find the documentation at https://docs.fastlane.tools 3 | // 4 | // For a list of all available actions, check out 5 | // 6 | // https://docs.fastlane.tools/actions 7 | // 8 | 9 | import Foundation 10 | 11 | class Fastfile: LaneFile { 12 | func releaseLane(withOptions options: [String: String]?) { 13 | desc("Build & pack a new release") 14 | 15 | // Constants 16 | let target = "SimpleDesktops" 17 | let version = options?["version"] ?? getVersionNumber(target: .userDefined(target)) 18 | let build = options?["build"] ?? getBuildNumber() 19 | let packageName = "\(target)_v\(version)" 20 | let outputDirectory = URL(fileURLWithPath: "./.build", isDirectory: true) 21 | 22 | // Bump version 23 | incrementVersionNumber(versionNumber: .userDefined(version)) 24 | incrementBuildNumber(buildNumber: .userDefined(build)) 25 | 26 | // Build app 27 | xcversion(version: "~> 13.1") 28 | let appPath = URL(fileURLWithPath: buildMacApp( 29 | scheme: .userDefined(target), 30 | outputDirectory: outputDirectory.path, 31 | codesigningIdentity: "-", 32 | exportMethod: "mac-application", 33 | xcodebuildFormatter: "xcpretty" 34 | )) 35 | 36 | // Move .app to folder (exclude .dSYM file) 37 | let packageDirectory = outputDirectory.appendingPathComponent( 38 | packageName, 39 | isDirectory: true 40 | ) 41 | try? FileManager.default.createDirectory( 42 | at: packageDirectory, 43 | withIntermediateDirectories: false, 44 | attributes: nil 45 | ) 46 | try? FileManager.default.moveItem( 47 | at: appPath, 48 | to: packageDirectory.appendingPathComponent(appPath.lastPathComponent) 49 | ) 50 | 51 | // Create DMG image 52 | let dmgPath = outputDirectory.appendingPathComponent("\(packageName).dmg") 53 | dmg(path: packageDirectory.path, outputPath: .userDefined(dmgPath.path), size: 10) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'fastlane-plugin-dmg' 6 | -------------------------------------------------------------------------------- /fastlane/Scanfile: -------------------------------------------------------------------------------- 1 | # For more information about this configuration visit 2 | # https://docs.fastlane.tools/actions/scan/#scanfile 3 | 4 | # In general, you can use the options available 5 | # fastlane scan --help 6 | 7 | # Remove the # in front of the line to enable the option 8 | 9 | scheme("SimpleDesktops") 10 | 11 | # open_report(true) 12 | 13 | # clean(true) 14 | 15 | # Enable skip_build to skip debug builds for faster test performance 16 | skip_build(true) 17 | -------------------------------------------------------------------------------- /fastlane/swift/Actions.swift: -------------------------------------------------------------------------------- 1 | import Foundation -------------------------------------------------------------------------------- /fastlane/swift/Appfile.swift: -------------------------------------------------------------------------------- 1 | // Appfile.swift 2 | // Copyright (c) 2021 FastlaneTools 3 | 4 | var appIdentifier: String { return "" } // The bundle identifier of your app 5 | var appleID: String { return "" } // Your Apple email address 6 | 7 | var teamID: String { return "" } // Developer Portal Team ID 8 | var itcTeam: String? { return nil } // App Store Connect Team ID (may be nil if no team) 9 | 10 | // you can even provide different app identifiers, Apple IDs and team names per lane: 11 | // More information: https://docs.fastlane.tools/advanced/#appfile 12 | 13 | // Please don't remove the lines below 14 | // They are used to detect outdated files 15 | // FastlaneRunnerAPIVersion [0.9.1] 16 | -------------------------------------------------------------------------------- /fastlane/swift/ArgumentProcessor.swift: -------------------------------------------------------------------------------- 1 | // ArgumentProcessor.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | // 5 | // ** NOTE ** 6 | // This file is provided by fastlane and WILL be overwritten in future updates 7 | // If you want to add extra functionality to this project, create a new file in a 8 | // new group so that it won't be marked for upgrade 9 | // 10 | 11 | import Foundation 12 | 13 | struct ArgumentProcessor { 14 | let args: [RunnerArgument] 15 | let currentLane: String 16 | let commandTimeout: Int 17 | let port: UInt32 18 | 19 | init(args: [String]) { 20 | // Dump the first arg which is the program name 21 | let fastlaneArgs = stride(from: 1, to: args.count - 1, by: 2).map { 22 | RunnerArgument(name: args[$0], value: args[$0 + 1]) 23 | } 24 | self.args = fastlaneArgs 25 | 26 | let fastlaneArgsMinusLanes = fastlaneArgs.filter { arg in 27 | arg.name.lowercased() != "lane" 28 | } 29 | 30 | let potentialLogMode = fastlaneArgsMinusLanes.filter { arg in 31 | arg.name.lowercased() == "logmode" 32 | } 33 | 34 | port = UInt32(fastlaneArgsMinusLanes.first(where: { $0.name == "swiftServerPort" })?.value ?? "") ?? 2000 35 | 36 | // Configure logMode since we might need to use it before we finish parsing 37 | if let logModeArg = potentialLogMode.first { 38 | let logModeString = logModeArg.value 39 | Logger.logMode = Logger.LogMode(logMode: logModeString) 40 | } 41 | 42 | let lanes = self.args.filter { arg in 43 | arg.name.lowercased() == "lane" 44 | } 45 | verbose(message: lanes.description) 46 | 47 | guard lanes.count == 1 else { 48 | let message = "You must have exactly one lane specified as an arg, here's what I got: \(lanes)" 49 | log(message: message) 50 | fatalError(message) 51 | } 52 | 53 | let lane = lanes.first! 54 | currentLane = lane.value 55 | 56 | // User might have configured a timeout for the socket connection 57 | let potentialTimeout = fastlaneArgsMinusLanes.filter { arg in 58 | arg.name.lowercased() == "timeoutseconds" 59 | } 60 | 61 | if let logModeArg = potentialLogMode.first { 62 | let logModeString = logModeArg.value 63 | Logger.logMode = Logger.LogMode(logMode: logModeString) 64 | } 65 | 66 | if let timeoutArg = potentialTimeout.first { 67 | let timeoutString = timeoutArg.value 68 | commandTimeout = (timeoutString as NSString).integerValue 69 | } else { 70 | commandTimeout = SocketClient.defaultCommandTimeoutSeconds 71 | } 72 | } 73 | 74 | func laneParameters() -> [String: String] { 75 | let laneParametersArgs = args.filter { arg in 76 | let lowercasedName = arg.name.lowercased() 77 | return lowercasedName != "timeoutseconds" && lowercasedName != "lane" && lowercasedName != "logmode" 78 | } 79 | var laneParameters = [String: String]() 80 | for arg in laneParametersArgs { 81 | laneParameters[arg.name] = arg.value 82 | } 83 | return laneParameters 84 | } 85 | } 86 | 87 | // Please don't remove the lines below 88 | // They are used to detect outdated files 89 | // FastlaneRunnerAPIVersion [0.9.2] 90 | -------------------------------------------------------------------------------- /fastlane/swift/Atomic.swift: -------------------------------------------------------------------------------- 1 | // Atomic.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | import Foundation 5 | 6 | protocol DictionaryProtocol: class { 7 | associatedtype Key: Hashable 8 | associatedtype Value 9 | 10 | subscript(_: Key) -> Value? { get set } 11 | 12 | @discardableResult 13 | func removeValue(forKey key: Key) -> Value? 14 | 15 | func get(_ key: Key) -> Value? 16 | func set(_ key: Key, value: Value?) 17 | } 18 | 19 | extension DictionaryProtocol { 20 | subscript(_ key: Key) -> Value? { 21 | get { 22 | get(key) 23 | } 24 | set { 25 | set(key, value: newValue) 26 | } 27 | } 28 | } 29 | 30 | protocol LockProtocol: DictionaryProtocol { 31 | associatedtype Lock 32 | 33 | var _lock: Lock { get set } 34 | 35 | func lock() 36 | func unlock() 37 | } 38 | 39 | protocol AnyLock {} 40 | 41 | extension UnsafeMutablePointer: AnyLock { 42 | @available(macOS, deprecated: 10.12) 43 | static func make() -> Self where Pointee == OSSpinLock { 44 | let spin = UnsafeMutablePointer.allocate(capacity: 1) 45 | spin.initialize(to: OS_SPINLOCK_INIT) 46 | return spin 47 | } 48 | 49 | @available(macOS, introduced: 10.12) 50 | static func make() -> Self where Pointee == os_unfair_lock { 51 | let unfairLock = UnsafeMutablePointer.allocate(capacity: 1) 52 | unfairLock.initialize(to: os_unfair_lock()) 53 | return unfairLock 54 | } 55 | 56 | @available(macOS, deprecated: 10.12) 57 | static func lock(_ lock: Self) where Pointee == OSSpinLock { 58 | OSSpinLockLock(lock) 59 | } 60 | 61 | @available(macOS, deprecated: 10.12) 62 | static func unlock(_ lock: Self) where Pointee == OSSpinLock { 63 | OSSpinLockUnlock(lock) 64 | } 65 | 66 | @available(macOS, introduced: 10.12) 67 | static func lock(_ lock: Self) where Pointee == os_unfair_lock { 68 | os_unfair_lock_lock(lock) 69 | } 70 | 71 | @available(macOS, introduced: 10.12) 72 | static func unlock(_ lock: Self) where Pointee == os_unfair_lock { 73 | os_unfair_lock_unlock(lock) 74 | } 75 | } 76 | 77 | // MARK: - Classes 78 | 79 | class AtomicDictionary: LockProtocol { 80 | typealias Lock = AnyLock 81 | 82 | var _lock: Lock 83 | 84 | private var storage: [Key: Value] = [:] 85 | 86 | init(_ lock: Lock) { 87 | _lock = lock 88 | } 89 | 90 | @discardableResult 91 | func removeValue(forKey key: Key) -> Value? { 92 | lock() 93 | defer { unlock() } 94 | return storage.removeValue(forKey: key) 95 | } 96 | 97 | func get(_ key: Key) -> Value? { 98 | lock() 99 | defer { unlock() } 100 | return storage[key] 101 | } 102 | 103 | func set(_ key: Key, value: Value?) { 104 | lock() 105 | defer { unlock() } 106 | storage[key] = value 107 | } 108 | 109 | func lock() { 110 | fatalError() 111 | } 112 | 113 | func unlock() { 114 | fatalError() 115 | } 116 | } 117 | 118 | @available(macOS, introduced: 10.12) 119 | final class UnfairAtomicDictionary: AtomicDictionary { 120 | typealias Lock = UnsafeMutablePointer 121 | 122 | init() { 123 | super.init(Lock.make()) 124 | } 125 | 126 | override func lock() { 127 | Lock.lock(_lock as! Lock) 128 | } 129 | 130 | override func unlock() { 131 | Lock.unlock(_lock as! Lock) 132 | } 133 | } 134 | 135 | @available(macOS, deprecated: 10.12) 136 | final class OSSPinAtomicDictionary: AtomicDictionary { 137 | typealias Lock = UnsafeMutablePointer 138 | 139 | init() { 140 | super.init(Lock.make()) 141 | } 142 | 143 | override func lock() { 144 | Lock.lock(_lock as! Lock) 145 | } 146 | 147 | override func unlock() { 148 | Lock.unlock(_lock as! Lock) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /fastlane/swift/ControlCommand.swift: -------------------------------------------------------------------------------- 1 | // ControlCommand.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | // 5 | // ** NOTE ** 6 | // This file is provided by fastlane and WILL be overwritten in future updates 7 | // If you want to add extra functionality to this project, create a new file in a 8 | // new group so that it won't be marked for upgrade 9 | // 10 | 11 | import Foundation 12 | 13 | struct ControlCommand: RubyCommandable { 14 | static let commandKey = "command" 15 | var type: CommandType { return .control } 16 | 17 | enum ShutdownCommandType { 18 | static let userMessageKey: String = "userMessage" 19 | 20 | enum CancelReason { 21 | static let reasonKey: String = "reason" 22 | case clientError 23 | case serverError 24 | 25 | var reasonText: String { 26 | switch self { 27 | case .clientError: 28 | return "clientError" 29 | case .serverError: 30 | return "serverError" 31 | } 32 | } 33 | } 34 | 35 | case done 36 | case cancel(cancelReason: CancelReason) 37 | 38 | var token: String { 39 | switch self { 40 | case .done: 41 | return "done" 42 | case .cancel: 43 | return "cancelFastlaneRun" 44 | } 45 | } 46 | } 47 | 48 | let message: String? 49 | let id: String = UUID().uuidString 50 | let shutdownCommandType: ShutdownCommandType 51 | var commandJson: String { 52 | var jsonDictionary: [String: Any] = [ControlCommand.commandKey: shutdownCommandType.token] 53 | 54 | if let message = message { 55 | jsonDictionary[ShutdownCommandType.userMessageKey] = message 56 | } 57 | if case let .cancel(reason) = shutdownCommandType { 58 | jsonDictionary[ShutdownCommandType.CancelReason.reasonKey] = reason.reasonText 59 | } 60 | 61 | let jsonData = try! JSONSerialization.data(withJSONObject: jsonDictionary, options: []) 62 | let jsonString = String(data: jsonData, encoding: .utf8)! 63 | return jsonString 64 | } 65 | 66 | init(commandType: ShutdownCommandType, message: String? = nil) { 67 | shutdownCommandType = commandType 68 | self.message = message 69 | } 70 | } 71 | 72 | // Please don't remove the lines below 73 | // They are used to detect outdated files 74 | // FastlaneRunnerAPIVersion [0.9.2] 75 | -------------------------------------------------------------------------------- /fastlane/swift/Deliverfile.swift: -------------------------------------------------------------------------------- 1 | // Deliverfile.swift 2 | // Copyright (c) 2021 FastlaneTools 3 | 4 | // This class is automatically included in FastlaneRunner during build 5 | 6 | // This autogenerated file will be overwritten or replaced during build time, or when you initialize `deliver` 7 | // 8 | // ** NOTE ** 9 | // This file is provided by fastlane and WILL be overwritten in future updates 10 | // If you want to add extra functionality to this project, create a new file in a 11 | // new group so that it won't be marked for upgrade 12 | // 13 | 14 | public class Deliverfile: DeliverfileProtocol { 15 | // If you want to enable `deliver`, run `fastlane deliver init` 16 | // After, this file will be replaced with a custom implementation that contains values you supplied 17 | // during the `init` process, and you won't see this message 18 | } 19 | 20 | // Generated with fastlane 2.195.0 21 | -------------------------------------------------------------------------------- /fastlane/swift/DeliverfileProtocol.swift: -------------------------------------------------------------------------------- 1 | // DeliverfileProtocol.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | public protocol DeliverfileProtocol: AnyObject { 5 | /// Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) 6 | var apiKeyPath: String? { get } 7 | 8 | /// Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) 9 | var apiKey: [String: Any]? { get } 10 | 11 | /// Your Apple ID Username 12 | var username: String? { get } 13 | 14 | /// The bundle identifier of your app 15 | var appIdentifier: String? { get } 16 | 17 | /// The version that should be edited or created 18 | var appVersion: String? { get } 19 | 20 | /// Path to your ipa file 21 | var ipa: String? { get } 22 | 23 | /// Path to your pkg file 24 | var pkg: String? { get } 25 | 26 | /// If set the given build number (already uploaded to iTC) will be used instead of the current built one 27 | var buildNumber: String? { get } 28 | 29 | /// The platform to use (optional) 30 | var platform: String { get } 31 | 32 | /// Modify live metadata, this option disables ipa upload and screenshot upload 33 | var editLive: Bool { get } 34 | 35 | /// Force usage of live version rather than edit version 36 | var useLiveVersion: Bool { get } 37 | 38 | /// Path to the folder containing the metadata files 39 | var metadataPath: String? { get } 40 | 41 | /// Path to the folder containing the screenshots 42 | var screenshotsPath: String? { get } 43 | 44 | /// Skip uploading an ipa or pkg to App Store Connect 45 | var skipBinaryUpload: Bool { get } 46 | 47 | /// Don't upload the screenshots 48 | var skipScreenshots: Bool { get } 49 | 50 | /// Don't upload the metadata (e.g. title, description). This will still upload screenshots 51 | var skipMetadata: Bool { get } 52 | 53 | /// Don’t create or update the app version that is being prepared for submission 54 | var skipAppVersionUpdate: Bool { get } 55 | 56 | /// Skip verification of HTML preview file 57 | var force: Bool { get } 58 | 59 | /// Clear all previously uploaded screenshots before uploading the new ones 60 | var overwriteScreenshots: Bool { get } 61 | 62 | /// Sync screenshots with local ones. This is currently beta optionso set true to 'FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS' environment variable as well 63 | var syncScreenshots: Bool { get } 64 | 65 | /// Submit the new version for Review after uploading everything 66 | var submitForReview: Bool { get } 67 | 68 | /// Verifies archive with App Store Connect without uploading 69 | var verifyOnly: Bool { get } 70 | 71 | /// Rejects the previously submitted build if it's in a state where it's possible 72 | var rejectIfPossible: Bool { get } 73 | 74 | /// Should the app be automatically released once it's approved? (Can not be used together with `auto_release_date`) 75 | var automaticRelease: Bool? { get } 76 | 77 | /// Date in milliseconds for automatically releasing on pending approval (Can not be used together with `automatic_release`) 78 | var autoReleaseDate: Int? { get } 79 | 80 | /// Enable the phased release feature of iTC 81 | var phasedRelease: Bool { get } 82 | 83 | /// Reset the summary rating when you release a new version of the application 84 | var resetRatings: Bool { get } 85 | 86 | /// The price tier of this application 87 | var priceTier: Int? { get } 88 | 89 | /// Path to the app rating's config 90 | var appRatingConfigPath: String? { get } 91 | 92 | /// Extra information for the submission (e.g. compliance specifications, IDFA settings) 93 | var submissionInformation: [String: Any]? { get } 94 | 95 | /// The ID of your App Store Connect team if you're in multiple teams 96 | var teamId: String? { get } 97 | 98 | /// The name of your App Store Connect team if you're in multiple teams 99 | var teamName: String? { get } 100 | 101 | /// The short ID of your Developer Portal team, if you're in multiple teams. Different from your iTC team ID! 102 | var devPortalTeamId: String? { get } 103 | 104 | /// The name of your Developer Portal team if you're in multiple teams 105 | var devPortalTeamName: String? { get } 106 | 107 | /// The provider short name to be used with the iTMSTransporter to identify your team. This value will override the automatically detected provider short name. To get provider short name run `pathToXcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u 'USERNAME' -p 'PASSWORD' -account_type itunes_connect -v off`. The short names of providers should be listed in the second column 108 | var itcProvider: String? { get } 109 | 110 | /// Run precheck before submitting to app review 111 | var runPrecheckBeforeSubmit: Bool { get } 112 | 113 | /// The default precheck rule level unless otherwise configured 114 | var precheckDefaultRuleLevel: String { get } 115 | 116 | /// **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - An array of localized metadata items to upload individually by language so that errors can be identified. E.g. ['name', 'keywords', 'description']. Note: slow 117 | var individualMetadataItems: [String]? { get } 118 | 119 | /// **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - Metadata: The path to the app icon 120 | var appIcon: String? { get } 121 | 122 | /// **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - Metadata: The path to the Apple Watch app icon 123 | var appleWatchAppIcon: String? { get } 124 | 125 | /// Metadata: The copyright notice 126 | var copyright: String? { get } 127 | 128 | /// Metadata: The english name of the primary category (e.g. `Business`, `Books`) 129 | var primaryCategory: String? { get } 130 | 131 | /// Metadata: The english name of the secondary category (e.g. `Business`, `Books`) 132 | var secondaryCategory: String? { get } 133 | 134 | /// Metadata: The english name of the primary first sub category (e.g. `Educational`, `Puzzle`) 135 | var primaryFirstSubCategory: String? { get } 136 | 137 | /// Metadata: The english name of the primary second sub category (e.g. `Educational`, `Puzzle`) 138 | var primarySecondSubCategory: String? { get } 139 | 140 | /// Metadata: The english name of the secondary first sub category (e.g. `Educational`, `Puzzle`) 141 | var secondaryFirstSubCategory: String? { get } 142 | 143 | /// Metadata: The english name of the secondary second sub category (e.g. `Educational`, `Puzzle`) 144 | var secondarySecondSubCategory: String? { get } 145 | 146 | /// **DEPRECATED!** This is no longer used by App Store Connect - Metadata: A hash containing the trade representative contact information 147 | var tradeRepresentativeContactInformation: [String: Any]? { get } 148 | 149 | /// Metadata: A hash containing the review information 150 | var appReviewInformation: [String: Any]? { get } 151 | 152 | /// Metadata: Path to the app review attachment file 153 | var appReviewAttachmentFile: String? { get } 154 | 155 | /// Metadata: The localised app description 156 | var description: [String: Any]? { get } 157 | 158 | /// Metadata: The localised app name 159 | var name: [String: Any]? { get } 160 | 161 | /// Metadata: The localised app subtitle 162 | var subtitle: [String: Any]? { get } 163 | 164 | /// Metadata: An array of localised keywords 165 | var keywords: [String: Any]? { get } 166 | 167 | /// Metadata: An array of localised promotional texts 168 | var promotionalText: [String: Any]? { get } 169 | 170 | /// Metadata: Localised release notes for this version 171 | var releaseNotes: [String: Any]? { get } 172 | 173 | /// Metadata: Localised privacy url 174 | var privacyUrl: [String: Any]? { get } 175 | 176 | /// Metadata: Localised Apple TV privacy policy text 177 | var appleTvPrivacyPolicy: [String: Any]? { get } 178 | 179 | /// Metadata: Localised support url 180 | var supportUrl: [String: Any]? { get } 181 | 182 | /// Metadata: Localised marketing url 183 | var marketingUrl: [String: Any]? { get } 184 | 185 | /// Metadata: List of languages to activate 186 | var languages: [String]? { get } 187 | 188 | /// Ignore errors when invalid languages are found in metadata and screenshot directories 189 | var ignoreLanguageDirectoryValidation: Bool { get } 190 | 191 | /// Should precheck check in-app purchases? 192 | var precheckIncludeInAppPurchases: Bool { get } 193 | 194 | /// The (spaceship) app ID of the app you want to use/modify 195 | var app: Int? { get } 196 | } 197 | 198 | public extension DeliverfileProtocol { 199 | var apiKeyPath: String? { return nil } 200 | var apiKey: [String: Any]? { return nil } 201 | var username: String? { return nil } 202 | var appIdentifier: String? { return nil } 203 | var appVersion: String? { return nil } 204 | var ipa: String? { return nil } 205 | var pkg: String? { return nil } 206 | var buildNumber: String? { return nil } 207 | var platform: String { return "ios" } 208 | var editLive: Bool { return false } 209 | var useLiveVersion: Bool { return false } 210 | var metadataPath: String? { return nil } 211 | var screenshotsPath: String? { return nil } 212 | var skipBinaryUpload: Bool { return false } 213 | var skipScreenshots: Bool { return false } 214 | var skipMetadata: Bool { return false } 215 | var skipAppVersionUpdate: Bool { return false } 216 | var force: Bool { return false } 217 | var overwriteScreenshots: Bool { return false } 218 | var syncScreenshots: Bool { return false } 219 | var submitForReview: Bool { return false } 220 | var verifyOnly: Bool { return false } 221 | var rejectIfPossible: Bool { return false } 222 | var automaticRelease: Bool? { return nil } 223 | var autoReleaseDate: Int? { return nil } 224 | var phasedRelease: Bool { return false } 225 | var resetRatings: Bool { return false } 226 | var priceTier: Int? { return nil } 227 | var appRatingConfigPath: String? { return nil } 228 | var submissionInformation: [String: Any]? { return nil } 229 | var teamId: String? { return nil } 230 | var teamName: String? { return nil } 231 | var devPortalTeamId: String? { return nil } 232 | var devPortalTeamName: String? { return nil } 233 | var itcProvider: String? { return nil } 234 | var runPrecheckBeforeSubmit: Bool { return true } 235 | var precheckDefaultRuleLevel: String { return "warn" } 236 | var individualMetadataItems: [String]? { return nil } 237 | var appIcon: String? { return nil } 238 | var appleWatchAppIcon: String? { return nil } 239 | var copyright: String? { return nil } 240 | var primaryCategory: String? { return nil } 241 | var secondaryCategory: String? { return nil } 242 | var primaryFirstSubCategory: String? { return nil } 243 | var primarySecondSubCategory: String? { return nil } 244 | var secondaryFirstSubCategory: String? { return nil } 245 | var secondarySecondSubCategory: String? { return nil } 246 | var tradeRepresentativeContactInformation: [String: Any]? { return nil } 247 | var appReviewInformation: [String: Any]? { return nil } 248 | var appReviewAttachmentFile: String? { return nil } 249 | var description: [String: Any]? { return nil } 250 | var name: [String: Any]? { return nil } 251 | var subtitle: [String: Any]? { return nil } 252 | var keywords: [String: Any]? { return nil } 253 | var promotionalText: [String: Any]? { return nil } 254 | var releaseNotes: [String: Any]? { return nil } 255 | var privacyUrl: [String: Any]? { return nil } 256 | var appleTvPrivacyPolicy: [String: Any]? { return nil } 257 | var supportUrl: [String: Any]? { return nil } 258 | var marketingUrl: [String: Any]? { return nil } 259 | var languages: [String]? { return nil } 260 | var ignoreLanguageDirectoryValidation: Bool { return false } 261 | var precheckIncludeInAppPurchases: Bool { return true } 262 | var app: Int? { return nil } 263 | } 264 | 265 | // Please don't remove the lines below 266 | // They are used to detect outdated files 267 | // FastlaneRunnerAPIVersion [0.9.107] 268 | -------------------------------------------------------------------------------- /fastlane/swift/Fastfile.swift: -------------------------------------------------------------------------------- 1 | // This class is automatically included in FastlaneRunner during build 2 | // If you have a custom Fastfile.swift, this file will be replaced by it 3 | // Don't modify this file unless you are familiar with how fastlane's swift code generation works 4 | // *** This file will be overwritten or replaced during build time *** 5 | 6 | import Foundation 7 | 8 | open class Fastfile: LaneFile { 9 | override public init() { 10 | super.init() 11 | } 12 | } 13 | 14 | // Please don't remove the lines below 15 | // They are used to detect outdated files 16 | // FastlaneRunnerAPIVersion [0.9.1] 17 | -------------------------------------------------------------------------------- /fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/xcshareddata/xcschemes/FastlaneRunner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 66 | 67 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /fastlane/swift/FastlaneSwiftRunner/README.txt: -------------------------------------------------------------------------------- 1 | Don't modify the structure of this group including but not limited to: 2 | - renaming this group 3 | - adding sub groups 4 | - removing sub groups 5 | - adding new files 6 | - removing files 7 | 8 | If you modify anything in this folder, future fastlane upgrades may not be able to be applied automatically. 9 | 10 | If you need to add new groups, please add them at the root of the "Fastlane Runner" group. 11 | -------------------------------------------------------------------------------- /fastlane/swift/Gymfile.swift: -------------------------------------------------------------------------------- 1 | // Gymfile.swift 2 | // Copyright (c) 2021 FastlaneTools 3 | 4 | // This class is automatically included in FastlaneRunner during build 5 | 6 | // This autogenerated file will be overwritten or replaced during build time, or when you initialize `gym` 7 | // 8 | // ** NOTE ** 9 | // This file is provided by fastlane and WILL be overwritten in future updates 10 | // If you want to add extra functionality to this project, create a new file in a 11 | // new group so that it won't be marked for upgrade 12 | // 13 | 14 | public class Gymfile: GymfileProtocol { 15 | // If you want to enable `gym`, run `fastlane gym init` 16 | // After, this file will be replaced with a custom implementation that contains values you supplied 17 | // during the `init` process, and you won't see this message 18 | } 19 | 20 | // Generated with fastlane 2.195.0 21 | -------------------------------------------------------------------------------- /fastlane/swift/GymfileProtocol.swift: -------------------------------------------------------------------------------- 1 | // GymfileProtocol.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | public protocol GymfileProtocol: AnyObject { 5 | /// Path to the workspace file 6 | var workspace: String? { get } 7 | 8 | /// Path to the project file 9 | var project: String? { get } 10 | 11 | /// The project's scheme. Make sure it's marked as `Shared` 12 | var scheme: String? { get } 13 | 14 | /// Should the project be cleaned before building it? 15 | var clean: Bool { get } 16 | 17 | /// The directory in which the ipa file should be stored in 18 | var outputDirectory: String { get } 19 | 20 | /// The name of the resulting ipa file 21 | var outputName: String? { get } 22 | 23 | /// The configuration to use when building the app. Defaults to 'Release' 24 | var configuration: String? { get } 25 | 26 | /// Hide all information that's not necessary while building 27 | var silent: Bool { get } 28 | 29 | /// The name of the code signing identity to use. It has to match the name exactly. e.g. 'iPhone Distribution: SunApps GmbH' 30 | var codesigningIdentity: String? { get } 31 | 32 | /// Should we skip packaging the ipa? 33 | var skipPackageIpa: Bool { get } 34 | 35 | /// Should we skip packaging the pkg? 36 | var skipPackagePkg: Bool { get } 37 | 38 | /// Should the ipa file include symbols? 39 | var includeSymbols: Bool? { get } 40 | 41 | /// Should the ipa file include bitcode? 42 | var includeBitcode: Bool? { get } 43 | 44 | /// Method used to export the archive. Valid values are: app-store, validation, ad-hoc, package, enterprise, development, developer-id and mac-application 45 | var exportMethod: String? { get } 46 | 47 | /// Path to an export options plist or a hash with export options. Use 'xcodebuild -help' to print the full set of available options 48 | var exportOptions: [String: Any]? { get } 49 | 50 | /// Pass additional arguments to xcodebuild for the package phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" 51 | var exportXcargs: String? { get } 52 | 53 | /// Export ipa from previously built xcarchive. Uses archive_path as source 54 | var skipBuildArchive: Bool? { get } 55 | 56 | /// After building, don't archive, effectively not including -archivePath param 57 | var skipArchive: Bool? { get } 58 | 59 | /// Build without codesigning 60 | var skipCodesigning: Bool? { get } 61 | 62 | /// Platform to build when using a Catalyst enabled app. Valid values are: ios, macos 63 | var catalystPlatform: String? { get } 64 | 65 | /// Full name of 3rd Party Mac Developer Installer or Developer ID Installer certificate. Example: `3rd Party Mac Developer Installer: Your Company (ABC1234XWYZ)` 66 | var installerCertName: String? { get } 67 | 68 | /// The directory in which the archive should be stored in 69 | var buildPath: String? { get } 70 | 71 | /// The path to the created archive 72 | var archivePath: String? { get } 73 | 74 | /// The directory where built products and other derived data will go 75 | var derivedDataPath: String? { get } 76 | 77 | /// Should an Xcode result bundle be generated in the output directory 78 | var resultBundle: Bool { get } 79 | 80 | /// Path to the result bundle directory to create. Ignored if `result_bundle` if false 81 | var resultBundlePath: String? { get } 82 | 83 | /// The directory where to store the build log 84 | var buildlogPath: String { get } 85 | 86 | /// The SDK that should be used for building the application 87 | var sdk: String? { get } 88 | 89 | /// The toolchain that should be used for building the application (e.g. com.apple.dt.toolchain.Swift_2_3, org.swift.30p620160816a) 90 | var toolchain: String? { get } 91 | 92 | /// Use a custom destination for building the app 93 | var destination: String? { get } 94 | 95 | /// Optional: Sometimes you need to specify a team id when exporting the ipa file 96 | var exportTeamId: String? { get } 97 | 98 | /// Pass additional arguments to xcodebuild for the build phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" 99 | var xcargs: String? { get } 100 | 101 | /// Use an extra XCCONFIG file to build your app 102 | var xcconfig: String? { get } 103 | 104 | /// Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path 105 | var suppressXcodeOutput: Bool? { get } 106 | 107 | /// xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) 108 | var xcodebuildFormatter: String { get } 109 | 110 | /// **DEPRECATED!** Use `xcodebuild_formatter: ''` instead - Disable xcpretty formatting of build output 111 | var disableXcpretty: Bool? { get } 112 | 113 | /// Use the test (RSpec style) format for build output 114 | var xcprettyTestFormat: Bool? { get } 115 | 116 | /// A custom xcpretty formatter to use 117 | var xcprettyFormatter: String? { get } 118 | 119 | /// Have xcpretty create a JUnit-style XML report at the provided path 120 | var xcprettyReportJunit: String? { get } 121 | 122 | /// Have xcpretty create a simple HTML report at the provided path 123 | var xcprettyReportHtml: String? { get } 124 | 125 | /// Have xcpretty create a JSON compilation database at the provided path 126 | var xcprettyReportJson: String? { get } 127 | 128 | /// Have xcpretty use unicode encoding when reporting builds 129 | var xcprettyUtf: Bool? { get } 130 | 131 | /// Analyze the project build time and store the output in 'culprits.txt' file 132 | var analyzeBuildTime: Bool? { get } 133 | 134 | /// Do not try to build a profile mapping from the xcodeproj. Match or a manually provided mapping should be used 135 | var skipProfileDetection: Bool { get } 136 | 137 | /// Allows for override of the default `xcodebuild` command 138 | var xcodebuildCommand: String { get } 139 | 140 | /// Sets a custom path for Swift Package Manager dependencies 141 | var clonedSourcePackagesPath: String? { get } 142 | 143 | /// Skips resolution of Swift Package Manager dependencies 144 | var skipPackageDependenciesResolution: Bool { get } 145 | 146 | /// Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file 147 | var disablePackageAutomaticUpdates: Bool { get } 148 | 149 | /// Lets xcodebuild use system's scm configuration 150 | var useSystemScm: Bool { get } 151 | } 152 | 153 | public extension GymfileProtocol { 154 | var workspace: String? { return nil } 155 | var project: String? { return nil } 156 | var scheme: String? { return nil } 157 | var clean: Bool { return false } 158 | var outputDirectory: String { return "." } 159 | var outputName: String? { return nil } 160 | var configuration: String? { return nil } 161 | var silent: Bool { return false } 162 | var codesigningIdentity: String? { return nil } 163 | var skipPackageIpa: Bool { return false } 164 | var skipPackagePkg: Bool { return false } 165 | var includeSymbols: Bool? { return nil } 166 | var includeBitcode: Bool? { return nil } 167 | var exportMethod: String? { return nil } 168 | var exportOptions: [String: Any]? { return nil } 169 | var exportXcargs: String? { return nil } 170 | var skipBuildArchive: Bool? { return nil } 171 | var skipArchive: Bool? { return nil } 172 | var skipCodesigning: Bool? { return nil } 173 | var catalystPlatform: String? { return nil } 174 | var installerCertName: String? { return nil } 175 | var buildPath: String? { return nil } 176 | var archivePath: String? { return nil } 177 | var derivedDataPath: String? { return nil } 178 | var resultBundle: Bool { return false } 179 | var resultBundlePath: String? { return nil } 180 | var buildlogPath: String { return "~/Library/Logs/gym" } 181 | var sdk: String? { return nil } 182 | var toolchain: String? { return nil } 183 | var destination: String? { return nil } 184 | var exportTeamId: String? { return nil } 185 | var xcargs: String? { return nil } 186 | var xcconfig: String? { return nil } 187 | var suppressXcodeOutput: Bool? { return nil } 188 | var xcodebuildFormatter: String { return "xcbeautify" } 189 | var disableXcpretty: Bool? { return nil } 190 | var xcprettyTestFormat: Bool? { return nil } 191 | var xcprettyFormatter: String? { return nil } 192 | var xcprettyReportJunit: String? { return nil } 193 | var xcprettyReportHtml: String? { return nil } 194 | var xcprettyReportJson: String? { return nil } 195 | var xcprettyUtf: Bool? { return nil } 196 | var analyzeBuildTime: Bool? { return nil } 197 | var skipProfileDetection: Bool { return false } 198 | var xcodebuildCommand: String { return "xcodebuild" } 199 | var clonedSourcePackagesPath: String? { return nil } 200 | var skipPackageDependenciesResolution: Bool { return false } 201 | var disablePackageAutomaticUpdates: Bool { return false } 202 | var useSystemScm: Bool { return false } 203 | } 204 | 205 | // Please don't remove the lines below 206 | // They are used to detect outdated files 207 | // FastlaneRunnerAPIVersion [0.9.110] 208 | -------------------------------------------------------------------------------- /fastlane/swift/LaneFileProtocol.swift: -------------------------------------------------------------------------------- 1 | // LaneFileProtocol.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | // 5 | // ** NOTE ** 6 | // This file is provided by fastlane and WILL be overwritten in future updates 7 | // If you want to add extra functionality to this project, create a new file in a 8 | // new group so that it won't be marked for upgrade 9 | // 10 | 11 | import Foundation 12 | 13 | public protocol LaneFileProtocol: AnyObject { 14 | var fastlaneVersion: String { get } 15 | static func runLane(from fastfile: LaneFile?, named lane: String, with parameters: [String: String]) -> Bool 16 | 17 | func recordLaneDescriptions() 18 | func beforeAll(with lane: String) 19 | func afterAll(with lane: String) 20 | func onError(currentLane: String, errorInfo: String, errorClass: String?, errorMessage: String?) 21 | } 22 | 23 | public extension LaneFileProtocol { 24 | var fastlaneVersion: String { return "" } // Defaults to "" because that means any is fine 25 | func beforeAll(with _: String) {} // No-op by default 26 | func afterAll(with _: String) {} // No-op by default 27 | func recordLaneDescriptions() {} // No-op by default 28 | } 29 | 30 | @objcMembers 31 | open class LaneFile: NSObject, LaneFileProtocol { 32 | private(set) static var fastfileInstance: LaneFile? 33 | private static var onErrorCalled = Set() 34 | 35 | private static func trimLaneFromName(laneName: String) -> String { 36 | return String(laneName.prefix(laneName.count - 4)) 37 | } 38 | 39 | private static func trimLaneWithOptionsFromName(laneName: String) -> String { 40 | return String(laneName.prefix(laneName.count - 12)) 41 | } 42 | 43 | public func onError(currentLane: String, errorInfo _: String, errorClass _: String?, errorMessage _: String?) { 44 | LaneFile.onErrorCalled.insert(currentLane) 45 | } 46 | 47 | private static var laneFunctionNames: [String] { 48 | var lanes: [String] = [] 49 | var methodCount: UInt32 = 0 50 | #if !SWIFT_PACKAGE 51 | let methodList = class_copyMethodList(self, &methodCount) 52 | #else 53 | // In SPM we're calling this functions out of the scope of the normal binary that it's 54 | // being built, so *self* in this scope would be the SPM executable instead of the Fastfile 55 | // that we'd normally expect. 56 | let methodList = class_copyMethodList(type(of: fastfileInstance!), &methodCount) 57 | #endif 58 | for i in 0 ..< Int(methodCount) { 59 | let selName = sel_getName(method_getName(methodList![i])) 60 | let name = String(cString: selName) 61 | let lowercasedName = name.lowercased() 62 | if lowercasedName.hasSuffix("lane") || lowercasedName.hasSuffix("lanewithoptions:") { 63 | lanes.append(name) 64 | } 65 | } 66 | return lanes 67 | } 68 | 69 | public static var lanes: [String: String] { 70 | var laneToMethodName: [String: String] = [:] 71 | laneFunctionNames.forEach { name in 72 | let lowercasedName = name.lowercased() 73 | if lowercasedName.hasSuffix("lane") { 74 | laneToMethodName[lowercasedName] = name 75 | let lowercasedNameNoLane = trimLaneFromName(laneName: lowercasedName) 76 | laneToMethodName[lowercasedNameNoLane] = name 77 | } else if lowercasedName.hasSuffix("lanewithoptions:") { 78 | let lowercasedNameNoOptions = trimLaneWithOptionsFromName(laneName: lowercasedName) 79 | laneToMethodName[lowercasedNameNoOptions] = name 80 | let lowercasedNameNoLane = trimLaneFromName(laneName: lowercasedNameNoOptions) 81 | laneToMethodName[lowercasedNameNoLane] = name 82 | } 83 | } 84 | 85 | return laneToMethodName 86 | } 87 | 88 | public static func loadFastfile() { 89 | if fastfileInstance == nil { 90 | let fastfileType: AnyObject.Type = NSClassFromString(className())! 91 | let fastfileAsNSObjectType: NSObject.Type = fastfileType as! NSObject.Type 92 | let currentFastfileInstance: Fastfile? = fastfileAsNSObjectType.init() as? Fastfile 93 | fastfileInstance = currentFastfileInstance 94 | } 95 | } 96 | 97 | public static func runLane(from fastfile: LaneFile?, named lane: String, with parameters: [String: String]) -> Bool { 98 | log(message: "Running lane: \(lane)") 99 | #if !SWIFT_PACKAGE 100 | // When not in SPM environment, we load the Fastfile from its `className()`. 101 | loadFastfile() 102 | guard let fastfileInstance = fastfileInstance as? Fastfile else { 103 | let message = "Unable to instantiate class named: \(className())" 104 | log(message: message) 105 | fatalError(message) 106 | } 107 | #else 108 | // When in SPM environment, we can't load the Fastfile from its `className()` because the executable is in 109 | // another scope, so `className()` won't be the expected Fastfile. Instead, we load the Fastfile as a Lanefile 110 | // in a static way, by parameter. 111 | guard let fastfileInstance = fastfile else { 112 | log(message: "Found nil instance of fastfile") 113 | preconditionFailure() 114 | } 115 | self.fastfileInstance = fastfileInstance 116 | #endif 117 | let currentLanes = lanes 118 | let lowerCasedLaneRequested = lane.lowercased() 119 | 120 | guard let laneMethod = currentLanes[lowerCasedLaneRequested] else { 121 | let laneNames = laneFunctionNames.map { laneFuctionName in 122 | if laneFuctionName.hasSuffix("lanewithoptions:") { 123 | return trimLaneWithOptionsFromName(laneName: laneFuctionName) 124 | } else { 125 | return trimLaneFromName(laneName: laneFuctionName) 126 | } 127 | }.joined(separator: ", ") 128 | 129 | let message = "[!] Could not find lane '\(lane)'. Available lanes: \(laneNames)" 130 | log(message: message) 131 | 132 | let shutdownCommand = ControlCommand(commandType: .cancel(cancelReason: .clientError), message: message) 133 | _ = runner.executeCommand(shutdownCommand) 134 | return false 135 | } 136 | 137 | // Call all methods that need to be called before we start calling lanes. 138 | fastfileInstance.beforeAll(with: lane) 139 | 140 | // We need to catch all possible errors here and display a nice message. 141 | _ = fastfileInstance.perform(NSSelectorFromString(laneMethod), with: parameters) 142 | 143 | // Call only on success. 144 | if !LaneFile.onErrorCalled.contains(lane) { 145 | fastfileInstance.afterAll(with: lane) 146 | } 147 | 148 | log(message: "Done running lane: \(lane) 🚀") 149 | return true 150 | } 151 | } 152 | 153 | // Please don't remove the lines below 154 | // They are used to detect outdated files 155 | // FastlaneRunnerAPIVersion [0.9.2] 156 | -------------------------------------------------------------------------------- /fastlane/swift/MainProcess.swift: -------------------------------------------------------------------------------- 1 | // MainProcess.swift 2 | // Copyright (c) 2021 FastlaneTools 3 | 4 | // 5 | // ** NOTE ** 6 | // This file is provided by fastlane and WILL be overwritten in future updates 7 | // If you want to add extra functionality to this project, create a new file in a 8 | // new group so that it won't be marked for upgrade 9 | // 10 | 11 | import Foundation 12 | #if canImport(SwiftShell) 13 | import SwiftShell 14 | #endif 15 | 16 | let argumentProcessor = ArgumentProcessor(args: CommandLine.arguments) 17 | let timeout = argumentProcessor.commandTimeout 18 | 19 | class MainProcess { 20 | var doneRunningLane = false 21 | var thread: Thread! 22 | #if SWIFT_PACKAGE 23 | var lastPrintDate = Date.distantFuture 24 | var timeBetweenPrints = Int.min 25 | var rubySocketCommand: AsyncCommand! 26 | #endif 27 | 28 | @objc func connectToFastlaneAndRunLane(_ fastfile: LaneFile?) { 29 | runner.startSocketThread(port: argumentProcessor.port) 30 | 31 | let completedRun = Fastfile.runLane(from: fastfile, named: argumentProcessor.currentLane, with: argumentProcessor.laneParameters()) 32 | if completedRun { 33 | runner.disconnectFromFastlaneProcess() 34 | } 35 | 36 | doneRunningLane = true 37 | } 38 | 39 | func startFastlaneThread(with fastFile: LaneFile?) { 40 | #if !SWIFT_PACKAGE 41 | thread = Thread(target: self, selector: #selector(connectToFastlaneAndRunLane), object: nil) 42 | #else 43 | thread = Thread(target: self, selector: #selector(connectToFastlaneAndRunLane), object: fastFile) 44 | #endif 45 | thread.name = "worker thread" 46 | #if SWIFT_PACKAGE 47 | let PATH = run("/bin/bash", "-c", "-l", "eval $(/usr/libexec/path_helper -s) ; echo $PATH").stdout 48 | main.env["PATH"] = PATH 49 | let path = main.run(bash: "which fastlane").stdout 50 | let pids = main.run("lsof", "-t", "-i", ":2000").stdout.split(separator: "\n") 51 | pids.forEach { main.run("kill", "-9", $0) } 52 | rubySocketCommand = main.runAsync(path, "socket_server", "-c", "1200") 53 | lastPrintDate = Date() 54 | rubySocketCommand.stderror.onStringOutput { print($0) } 55 | rubySocketCommand.stdout.onStringOutput { stdout in 56 | print(stdout) 57 | self.timeBetweenPrints = Int(self.lastPrintDate.timeIntervalSinceNow) 58 | } 59 | 60 | // swiftformat:disable:next redundantSelf 61 | _ = Runner.waitWithPolling(self.timeBetweenPrints, toEventually: { $0 > 5 }, timeout: 10) 62 | thread.start() 63 | #endif 64 | } 65 | } 66 | 67 | public class Main { 68 | let process = MainProcess() 69 | 70 | public init() {} 71 | 72 | public func run(with fastFile: LaneFile?) { 73 | process.startFastlaneThread(with: fastFile) 74 | 75 | while !process.doneRunningLane, RunLoop.current.run(mode: RunLoopMode.defaultRunLoopMode, before: Date(timeIntervalSinceNow: 2)) { 76 | // no op 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /fastlane/swift/Matchfile.swift: -------------------------------------------------------------------------------- 1 | // Matchfile.swift 2 | // Copyright (c) 2021 FastlaneTools 3 | 4 | // This class is automatically included in FastlaneRunner during build 5 | 6 | // This autogenerated file will be overwritten or replaced during build time, or when you initialize `match` 7 | // 8 | // ** NOTE ** 9 | // This file is provided by fastlane and WILL be overwritten in future updates 10 | // If you want to add extra functionality to this project, create a new file in a 11 | // new group so that it won't be marked for upgrade 12 | // 13 | 14 | public class Matchfile: MatchfileProtocol { 15 | // If you want to enable `match`, run `fastlane match init` 16 | // After, this file will be replaced with a custom implementation that contains values you supplied 17 | // during the `init` process, and you won't see this message 18 | } 19 | 20 | // Generated with fastlane 2.195.0 21 | -------------------------------------------------------------------------------- /fastlane/swift/MatchfileProtocol.swift: -------------------------------------------------------------------------------- 1 | // MatchfileProtocol.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | public protocol MatchfileProtocol: AnyObject { 5 | /// Define the profile type, can be appstore, adhoc, development, enterprise, developer_id, mac_installer_distribution 6 | var type: String { get } 7 | 8 | /// Create additional cert types needed for macOS installers (valid values: mac_installer_distribution, developer_id_installer) 9 | var additionalCertTypes: [String]? { get } 10 | 11 | /// Only fetch existing certificates and profiles, don't generate new ones 12 | var readonly: Bool { get } 13 | 14 | /// Create a certificate type for Xcode 11 and later (Apple Development or Apple Distribution) 15 | var generateAppleCerts: Bool { get } 16 | 17 | /// Skip syncing provisioning profiles 18 | var skipProvisioningProfiles: Bool { get } 19 | 20 | /// The bundle identifier(s) of your app (comma-separated string or array of strings) 21 | var appIdentifier: [String] { get } 22 | 23 | /// Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) 24 | var apiKeyPath: String? { get } 25 | 26 | /// Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) 27 | var apiKey: [String: Any]? { get } 28 | 29 | /// Your Apple ID Username 30 | var username: String? { get } 31 | 32 | /// The ID of your Developer Portal team if you're in multiple teams 33 | var teamId: String? { get } 34 | 35 | /// The name of your Developer Portal team if you're in multiple teams 36 | var teamName: String? { get } 37 | 38 | /// Define where you want to store your certificates 39 | var storageMode: String { get } 40 | 41 | /// URL to the git repo containing all the certificates 42 | var gitUrl: String { get } 43 | 44 | /// Specific git branch to use 45 | var gitBranch: String { get } 46 | 47 | /// git user full name to commit 48 | var gitFullName: String? { get } 49 | 50 | /// git user email to commit 51 | var gitUserEmail: String? { get } 52 | 53 | /// Make a shallow clone of the repository (truncate the history to 1 revision) 54 | var shallowClone: Bool { get } 55 | 56 | /// Clone just the branch specified, instead of the whole repo. This requires that the branch already exists. Otherwise the command will fail 57 | var cloneBranchDirectly: Bool { get } 58 | 59 | /// Use a basic authorization header to access the git repo (e.g.: access via HTTPS, GitHub Actions, etc), usually a string in Base64 60 | var gitBasicAuthorization: String? { get } 61 | 62 | /// Use a bearer authorization header to access the git repo (e.g.: access to an Azure DevOps repository), usually a string in Base64 63 | var gitBearerAuthorization: String? { get } 64 | 65 | /// Use a private key to access the git repo (e.g.: access to GitHub repository via Deploy keys), usually a id_rsa named file or the contents hereof 66 | var gitPrivateKey: String? { get } 67 | 68 | /// Name of the Google Cloud Storage bucket to use 69 | var googleCloudBucketName: String? { get } 70 | 71 | /// Path to the gc_keys.json file 72 | var googleCloudKeysFile: String? { get } 73 | 74 | /// ID of the Google Cloud project to use for authentication 75 | var googleCloudProjectId: String? { get } 76 | 77 | /// Skips confirming to use the system google account 78 | var skipGoogleCloudAccountConfirmation: Bool { get } 79 | 80 | /// Name of the S3 region 81 | var s3Region: String? { get } 82 | 83 | /// S3 access key 84 | var s3AccessKey: String? { get } 85 | 86 | /// S3 secret access key 87 | var s3SecretAccessKey: String? { get } 88 | 89 | /// Name of the S3 bucket 90 | var s3Bucket: String? { get } 91 | 92 | /// Prefix to be used on all objects uploaded to S3 93 | var s3ObjectPrefix: String? { get } 94 | 95 | /// GitLab Project Path (i.e. 'gitlab-org/gitlab') 96 | var gitlabProject: String? { get } 97 | 98 | /// Keychain the items should be imported to 99 | var keychainName: String { get } 100 | 101 | /// This might be required the first time you access certificates on a new mac. For the login/default keychain this is your macOS account password 102 | var keychainPassword: String? { get } 103 | 104 | /// Renew the provisioning profiles every time you run match 105 | var force: Bool { get } 106 | 107 | /// Renew the provisioning profiles if the device count on the developer portal has changed. Ignored for profile types 'appstore' and 'developer_id' 108 | var forceForNewDevices: Bool { get } 109 | 110 | /// Include all matching certificates in the provisioning profile. Works only for the 'development' provisioning profile type 111 | var includeAllCertificates: Bool { get } 112 | 113 | /// Renew the provisioning profiles if the certificate count on the developer portal has changed. Works only for the 'development' provisioning profile type. Requires 'include_all_certificates' option to be 'true' 114 | var forceForNewCertificates: Bool { get } 115 | 116 | /// Disables confirmation prompts during nuke, answering them with yes 117 | var skipConfirmation: Bool { get } 118 | 119 | /// Remove certs from repository during nuke without revoking them on the developer portal 120 | var safeRemoveCerts: Bool { get } 121 | 122 | /// Skip generation of a README.md for the created git repository 123 | var skipDocs: Bool { get } 124 | 125 | /// Set the provisioning profile's platform to work with (i.e. ios, tvos, macos, catalyst) 126 | var platform: String { get } 127 | 128 | /// Enable this if you have the Mac Catalyst capability enabled and your project was created with Xcode 11.3 or earlier. Prepends 'maccatalyst.' to the app identifier for the provisioning profile mapping 129 | var deriveCatalystAppIdentifier: Bool { get } 130 | 131 | /// The name of provisioning profile template. If the developer account has provisioning profile templates (aka: custom entitlements), the template name can be found by inspecting the Entitlements drop-down while creating/editing a provisioning profile (e.g. "Apple Pay Pass Suppression Development") 132 | var templateName: String? { get } 133 | 134 | /// A custom name for the provisioning profile. This will replace the default provisioning profile name if specified 135 | var profileName: String? { get } 136 | 137 | /// Should the command fail if it was about to create a duplicate of an existing provisioning profile. It can happen due to issues on Apple Developer Portal, when profile to be recreated was not properly deleted first 138 | var failOnNameTaken: Bool { get } 139 | 140 | /// Set to true if there is no access to Apple developer portal but there are certificates, keys and profiles provided. Only works with match import action 141 | var skipCertificateMatching: Bool { get } 142 | 143 | /// Path in which to export certificates, key and profile 144 | var outputPath: String? { get } 145 | 146 | /// Skips setting the partition list (which can sometimes take a long time). Setting the partition list is usually needed to prevent Xcode from prompting to allow a cert to be used for signing 147 | var skipSetPartitionList: Bool { get } 148 | 149 | /// Print out extra information and all commands 150 | var verbose: Bool { get } 151 | } 152 | 153 | public extension MatchfileProtocol { 154 | var type: String { return "development" } 155 | var additionalCertTypes: [String]? { return nil } 156 | var readonly: Bool { return false } 157 | var generateAppleCerts: Bool { return true } 158 | var skipProvisioningProfiles: Bool { return false } 159 | var appIdentifier: [String] { return [] } 160 | var apiKeyPath: String? { return nil } 161 | var apiKey: [String: Any]? { return nil } 162 | var username: String? { return nil } 163 | var teamId: String? { return nil } 164 | var teamName: String? { return nil } 165 | var storageMode: String { return "git" } 166 | var gitUrl: String { return "" } 167 | var gitBranch: String { return "master" } 168 | var gitFullName: String? { return nil } 169 | var gitUserEmail: String? { return nil } 170 | var shallowClone: Bool { return false } 171 | var cloneBranchDirectly: Bool { return false } 172 | var gitBasicAuthorization: String? { return nil } 173 | var gitBearerAuthorization: String? { return nil } 174 | var gitPrivateKey: String? { return nil } 175 | var googleCloudBucketName: String? { return nil } 176 | var googleCloudKeysFile: String? { return nil } 177 | var googleCloudProjectId: String? { return nil } 178 | var skipGoogleCloudAccountConfirmation: Bool { return false } 179 | var s3Region: String? { return nil } 180 | var s3AccessKey: String? { return nil } 181 | var s3SecretAccessKey: String? { return nil } 182 | var s3Bucket: String? { return nil } 183 | var s3ObjectPrefix: String? { return nil } 184 | var gitlabProject: String? { return nil } 185 | var keychainName: String { return "login.keychain" } 186 | var keychainPassword: String? { return nil } 187 | var force: Bool { return false } 188 | var forceForNewDevices: Bool { return false } 189 | var includeAllCertificates: Bool { return false } 190 | var forceForNewCertificates: Bool { return false } 191 | var skipConfirmation: Bool { return false } 192 | var safeRemoveCerts: Bool { return false } 193 | var skipDocs: Bool { return false } 194 | var platform: String { return "ios" } 195 | var deriveCatalystAppIdentifier: Bool { return false } 196 | var templateName: String? { return nil } 197 | var profileName: String? { return nil } 198 | var failOnNameTaken: Bool { return false } 199 | var skipCertificateMatching: Bool { return false } 200 | var outputPath: String? { return nil } 201 | var skipSetPartitionList: Bool { return false } 202 | var verbose: Bool { return false } 203 | } 204 | 205 | // Please don't remove the lines below 206 | // They are used to detect outdated files 207 | // FastlaneRunnerAPIVersion [0.9.104] 208 | -------------------------------------------------------------------------------- /fastlane/swift/OptionalConfigValue.swift: -------------------------------------------------------------------------------- 1 | // OptionalConfigValue.swift 2 | // Copyright (c) 2021 FastlaneTools 3 | 4 | // 5 | // ** NOTE ** 6 | // This file is provided by fastlane and WILL be overwritten in future updates 7 | // If you want to add extra functionality to this project, create a new file in a 8 | // new group so that it won't be marked for upgrade 9 | // 10 | 11 | import Foundation 12 | 13 | public enum OptionalConfigValue { 14 | case fastlaneDefault(T) 15 | case userDefined(T) 16 | case `nil` 17 | 18 | func asRubyArgument(name: String, type: RubyCommand.Argument.ArgType? = nil) -> RubyCommand.Argument? { 19 | if case let .userDefined(value) = self { 20 | return RubyCommand.Argument(name: name, value: value, type: type) 21 | } 22 | return nil 23 | } 24 | } 25 | 26 | extension OptionalConfigValue: ExpressibleByUnicodeScalarLiteral where T == String? { 27 | public typealias UnicodeScalarLiteralType = String 28 | 29 | public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { 30 | self = .userDefined(value) 31 | } 32 | } 33 | 34 | extension OptionalConfigValue: ExpressibleByExtendedGraphemeClusterLiteral where T == String? { 35 | public typealias ExtendedGraphemeClusterLiteralType = String 36 | 37 | public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { 38 | self = .userDefined(value) 39 | } 40 | } 41 | 42 | extension OptionalConfigValue: ExpressibleByStringLiteral where T == String? { 43 | public typealias StringLiteralType = String 44 | 45 | public init(stringLiteral value: StringLiteralType) { 46 | self = .userDefined(value) 47 | } 48 | } 49 | 50 | extension OptionalConfigValue: ExpressibleByStringInterpolation where T == String? {} 51 | 52 | extension OptionalConfigValue: ExpressibleByNilLiteral { 53 | public init(nilLiteral _: ()) { 54 | self = .nil 55 | } 56 | } 57 | 58 | extension OptionalConfigValue: ExpressibleByIntegerLiteral where T == Int? { 59 | public typealias IntegerLiteralType = Int 60 | 61 | public init(integerLiteral value: IntegerLiteralType) { 62 | self = .userDefined(value) 63 | } 64 | } 65 | 66 | extension OptionalConfigValue: ExpressibleByArrayLiteral where T == [String] { 67 | public typealias ArrayLiteralElement = String 68 | 69 | public init(arrayLiteral elements: ArrayLiteralElement...) { 70 | self = .userDefined(elements) 71 | } 72 | } 73 | 74 | extension OptionalConfigValue: ExpressibleByFloatLiteral where T == Float { 75 | public typealias FloatLiteralType = Float 76 | 77 | public init(floatLiteral value: FloatLiteralType) { 78 | self = .userDefined(value) 79 | } 80 | } 81 | 82 | extension OptionalConfigValue: ExpressibleByBooleanLiteral where T == Bool { 83 | public typealias BooleanLiteralType = Bool 84 | 85 | public init(booleanLiteral value: BooleanLiteralType) { 86 | self = .userDefined(value) 87 | } 88 | } 89 | 90 | extension OptionalConfigValue: ExpressibleByDictionaryLiteral where T == [String: Any] { 91 | public typealias Key = String 92 | public typealias Value = Any 93 | 94 | public init(dictionaryLiteral elements: (Key, Value)...) { 95 | var dict: [Key: Value] = [:] 96 | elements.forEach { 97 | dict[$0.0] = $0.1 98 | } 99 | self = .userDefined(dict) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /fastlane/swift/Plugins.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | /** 3 | Create DMG for your Mac app 4 | 5 | - parameters: 6 | - path: Path to the directory to be archived to dmg 7 | - outputPath: The name of the resulting dmg file 8 | - volumeName: The volume name of the resulting image 9 | - filesystem: The filesystem of the resulting image 10 | - format: The format of the resulting image 11 | - size: Size of the resulting dmg file in megabytes 12 | 13 | - returns: The path of the output dmg file 14 | 15 | Use this action to create dmg for Mac app 16 | */ 17 | public func dmg(path: String, 18 | outputPath: OptionalConfigValue = .fastlaneDefault(nil), 19 | volumeName: OptionalConfigValue = .fastlaneDefault(nil), 20 | filesystem: String = "HFS+", 21 | format: String = "UDZO", 22 | size: OptionalConfigValue = .fastlaneDefault(nil)) { 23 | let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) 24 | let outputPathArg = outputPath.asRubyArgument(name: "output_path", type: nil) 25 | let volumeNameArg = volumeName.asRubyArgument(name: "volume_name", type: nil) 26 | let filesystemArg = RubyCommand.Argument(name: "filesystem", value: filesystem, type: nil) 27 | let formatArg = RubyCommand.Argument(name: "format", value: format, type: nil) 28 | let sizeArg = size.asRubyArgument(name: "size", type: nil) 29 | let array: [RubyCommand.Argument?] = [pathArg, 30 | outputPathArg, 31 | volumeNameArg, 32 | filesystemArg, 33 | formatArg, 34 | sizeArg] 35 | let args: [RubyCommand.Argument] = array 36 | .filter { $0?.value != nil } 37 | .compactMap { $0 } 38 | let command = RubyCommand(commandID: "", methodName: "dmg", className: nil, args: args) 39 | _ = runner.executeCommand(command) 40 | } 41 | -------------------------------------------------------------------------------- /fastlane/swift/Precheckfile.swift: -------------------------------------------------------------------------------- 1 | // Precheckfile.swift 2 | // Copyright (c) 2021 FastlaneTools 3 | 4 | // This class is automatically included in FastlaneRunner during build 5 | 6 | // This autogenerated file will be overwritten or replaced during build time, or when you initialize `precheck` 7 | // 8 | // ** NOTE ** 9 | // This file is provided by fastlane and WILL be overwritten in future updates 10 | // If you want to add extra functionality to this project, create a new file in a 11 | // new group so that it won't be marked for upgrade 12 | // 13 | 14 | public class Precheckfile: PrecheckfileProtocol { 15 | // If you want to enable `precheck`, run `fastlane precheck init` 16 | // After, this file will be replaced with a custom implementation that contains values you supplied 17 | // during the `init` process, and you won't see this message 18 | } 19 | 20 | // Generated with fastlane 2.195.0 21 | -------------------------------------------------------------------------------- /fastlane/swift/PrecheckfileProtocol.swift: -------------------------------------------------------------------------------- 1 | // PrecheckfileProtocol.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | public protocol PrecheckfileProtocol: AnyObject { 5 | /// Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) 6 | var apiKeyPath: String? { get } 7 | 8 | /// Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) 9 | var apiKey: [String: Any]? { get } 10 | 11 | /// The bundle identifier of your app 12 | var appIdentifier: String { get } 13 | 14 | /// Your Apple ID Username 15 | var username: String? { get } 16 | 17 | /// The ID of your App Store Connect team if you're in multiple teams 18 | var teamId: String? { get } 19 | 20 | /// The name of your App Store Connect team if you're in multiple teams 21 | var teamName: String? { get } 22 | 23 | /// The platform to use (optional) 24 | var platform: String { get } 25 | 26 | /// The default rule level unless otherwise configured 27 | var defaultRuleLevel: String { get } 28 | 29 | /// Should check in-app purchases? 30 | var includeInAppPurchases: Bool { get } 31 | 32 | /// Should force check live app? 33 | var useLive: Bool { get } 34 | 35 | /// using text indicating that your IAP is free 36 | var freeStuffInIap: String? { get } 37 | } 38 | 39 | public extension PrecheckfileProtocol { 40 | var apiKeyPath: String? { return nil } 41 | var apiKey: [String: Any]? { return nil } 42 | var appIdentifier: String { return "" } 43 | var username: String? { return nil } 44 | var teamId: String? { return nil } 45 | var teamName: String? { return nil } 46 | var platform: String { return "ios" } 47 | var defaultRuleLevel: String { return "error" } 48 | var includeInAppPurchases: Bool { return true } 49 | var useLive: Bool { return false } 50 | var freeStuffInIap: String? { return nil } 51 | } 52 | 53 | // Please don't remove the lines below 54 | // They are used to detect outdated files 55 | // FastlaneRunnerAPIVersion [0.9.103] 56 | -------------------------------------------------------------------------------- /fastlane/swift/RubyCommand.swift: -------------------------------------------------------------------------------- 1 | // RubyCommand.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | // 5 | // ** NOTE ** 6 | // This file is provided by fastlane and WILL be overwritten in future updates 7 | // If you want to add extra functionality to this project, create a new file in a 8 | // new group so that it won't be marked for upgrade 9 | // 10 | 11 | import Foundation 12 | 13 | struct RubyCommand: RubyCommandable { 14 | var type: CommandType { return .action } 15 | 16 | struct Argument { 17 | enum ArgType { 18 | case stringClosure 19 | 20 | var typeString: String { 21 | switch self { 22 | case .stringClosure: 23 | return "string_closure" // this should match when is in ruby's SocketServerActionCommandExecutor 24 | } 25 | } 26 | } 27 | 28 | let name: String 29 | let value: Any? 30 | let type: ArgType? 31 | 32 | init(name: String, value: Any?, type: ArgType? = nil) { 33 | self.name = name 34 | self.value = value 35 | self.type = type 36 | } 37 | 38 | var hasValue: Bool { 39 | return value != nil 40 | } 41 | 42 | var json: String { 43 | if let someValue = value { 44 | let typeJson: String 45 | if let type = type { 46 | typeJson = ", \"value_type\" : \"\(type.typeString)\"" 47 | } else { 48 | typeJson = "" 49 | } 50 | 51 | if type == .stringClosure { 52 | return "{\"name\" : \"\(name)\", \"value\" : \"ignored_for_closure\"\(typeJson)}" 53 | } else if let array = someValue as? [String] { 54 | return "{\"name\" : \"\(name)\", \"value\" : \(array)\(typeJson)}" 55 | } else if let hash = someValue as? [String: Any] { 56 | let jsonData = try! JSONSerialization.data(withJSONObject: hash, options: []) 57 | let jsonString = String(data: jsonData, encoding: .utf8)! 58 | return "{\"name\" : \"\(name)\", \"value\" : \(jsonString)\(typeJson)}" 59 | } else { 60 | let dictionary = [ 61 | "name": name, 62 | "value": someValue, 63 | ] 64 | let jsonData = try! JSONSerialization.data(withJSONObject: dictionary, options: []) 65 | let jsonString = String(data: jsonData, encoding: .utf8)! 66 | return jsonString 67 | } 68 | } else { 69 | // Just exclude this arg if it doesn't have a value 70 | return "" 71 | } 72 | } 73 | } 74 | 75 | let commandID: String 76 | let methodName: String 77 | let className: String? 78 | let args: [Argument] 79 | let id: String = UUID().uuidString 80 | 81 | var closure: ((String) -> Void)? { 82 | let callbacks = args.filter { ($0.type != nil) && $0.type == .stringClosure } 83 | guard let callback = callbacks.first else { 84 | return nil 85 | } 86 | 87 | guard let callbackArgValue = callback.value else { 88 | return nil 89 | } 90 | 91 | guard let callbackClosure = callbackArgValue as? ((String) -> Void) else { 92 | return nil 93 | } 94 | return callbackClosure 95 | } 96 | 97 | func callbackClosure(_ callbackArg: String) -> ((String) -> Void)? { 98 | // WARNING: This will perform the first callback it receives 99 | let callbacks = args.filter { ($0.type != nil) && $0.type == .stringClosure } 100 | guard let callback = callbacks.first else { 101 | verbose(message: "received call to performCallback with \(callbackArg), but no callback available to perform") 102 | return nil 103 | } 104 | 105 | guard let callbackArgValue = callback.value else { 106 | verbose(message: "received call to performCallback with \(callbackArg), but callback is nil") 107 | return nil 108 | } 109 | 110 | guard let callbackClosure = callbackArgValue as? ((String) -> Void) else { 111 | verbose(message: "received call to performCallback with \(callbackArg), but callback type is unknown \(callbackArgValue.self)") 112 | return nil 113 | } 114 | return callbackClosure 115 | } 116 | 117 | func performCallback(callbackArg: String, socket: SocketClient, completion: @escaping () -> Void) { 118 | verbose(message: "Performing callback with: \(callbackArg)") 119 | socket.leave() 120 | callbackClosure(callbackArg)?(callbackArg) 121 | completion() 122 | } 123 | 124 | var commandJson: String { 125 | let argsArrayJson = args 126 | .map { $0.json } 127 | .filter { $0 != "" } 128 | 129 | let argsJson: String? 130 | if !argsArrayJson.isEmpty { 131 | argsJson = "\"args\" : [\(argsArrayJson.joined(separator: ","))]" 132 | } else { 133 | argsJson = nil 134 | } 135 | 136 | let commandIDJson = "\"commandID\" : \"\(commandID)\"" 137 | let methodNameJson = "\"methodName\" : \"\(methodName)\"" 138 | 139 | var jsonParts = [commandIDJson, methodNameJson] 140 | if let argsJson = argsJson { 141 | jsonParts.append(argsJson) 142 | } 143 | 144 | if let className = className { 145 | let classNameJson = "\"className\" : \"\(className)\"" 146 | jsonParts.append(classNameJson) 147 | } 148 | 149 | let commandJsonString = "{\(jsonParts.joined(separator: ","))}" 150 | 151 | return commandJsonString 152 | } 153 | } 154 | 155 | // Please don't remove the lines below 156 | // They are used to detect outdated files 157 | // FastlaneRunnerAPIVersion [0.9.2] 158 | -------------------------------------------------------------------------------- /fastlane/swift/RubyCommandable.swift: -------------------------------------------------------------------------------- 1 | // RubyCommandable.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | // 5 | // ** NOTE ** 6 | // This file is provided by fastlane and WILL be overwritten in future updates 7 | // If you want to add extra functionality to this project, create a new file in a 8 | // new group so that it won't be marked for upgrade 9 | // 10 | 11 | import Foundation 12 | 13 | enum CommandType { 14 | case action 15 | case control 16 | 17 | var token: String { 18 | switch self { 19 | case .action: 20 | return "action" 21 | case .control: 22 | return "control" 23 | } 24 | } 25 | } 26 | 27 | protocol RubyCommandable { 28 | var type: CommandType { get } 29 | var commandJson: String { get } 30 | var id: String { get } 31 | } 32 | 33 | extension RubyCommandable { 34 | var json: String { 35 | return """ 36 | { "commandType": "\(type.token)", "command": \(commandJson) } 37 | """ 38 | } 39 | } 40 | 41 | // Please don't remove the lines below 42 | // They are used to detect outdated files 43 | // FastlaneRunnerAPIVersion [0.9.2] 44 | -------------------------------------------------------------------------------- /fastlane/swift/Runner.swift: -------------------------------------------------------------------------------- 1 | // Runner.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | // 5 | // ** NOTE ** 6 | // This file is provided by fastlane and WILL be overwritten in future updates 7 | // If you want to add extra functionality to this project, create a new file in a 8 | // new group so that it won't be marked for upgrade 9 | // 10 | 11 | import Foundation 12 | 13 | let logger: Logger = .init() 14 | 15 | let runner: Runner = .init() 16 | 17 | func desc(_: String) { 18 | // no-op, this is handled in fastlane/lane_list.rb 19 | } 20 | 21 | class Runner { 22 | private var thread: Thread! 23 | private var socketClient: SocketClient! 24 | private let dispatchGroup = DispatchGroup() 25 | private var returnValue: String? // lol, so safe 26 | private var currentlyExecutingCommand: RubyCommandable? 27 | private var shouldLeaveDispatchGroupDuringDisconnect = false 28 | private var executeNext: AtomicDictionary = { 29 | if #available(macOS 10.12, *) { 30 | return UnfairAtomicDictionary() 31 | } else { 32 | return OSSPinAtomicDictionary() 33 | } 34 | }() 35 | 36 | func executeCommand(_ command: RubyCommandable) -> String { 37 | dispatchGroup.enter() 38 | currentlyExecutingCommand = command 39 | socketClient.send(rubyCommand: command) 40 | 41 | let secondsToWait = DispatchTimeInterval.seconds(SocketClient.defaultCommandTimeoutSeconds) 42 | // swiftformat:disable:next redundantSelf 43 | let timeoutResult = Self.waitWithPolling(self.executeNext[command.id], toEventually: { $0 == true }, timeout: SocketClient.defaultCommandTimeoutSeconds) 44 | executeNext.removeValue(forKey: command.id) 45 | let failureMessage = "command didn't execute in: \(SocketClient.defaultCommandTimeoutSeconds) seconds" 46 | let success = testDispatchTimeoutResult(timeoutResult, failureMessage: failureMessage, timeToWait: secondsToWait) 47 | guard success else { 48 | log(message: "command timeout") 49 | preconditionFailure() 50 | } 51 | 52 | if let _returnValue = returnValue { 53 | return _returnValue 54 | } else { 55 | return "" 56 | } 57 | } 58 | 59 | static func waitWithPolling(_ expression: @autoclosure @escaping () throws -> T, toEventually predicate: @escaping (T) -> Bool, timeout: Int, pollingInterval: DispatchTimeInterval = .milliseconds(4)) -> DispatchTimeoutResult { 60 | func memoizedClosure(_ closure: @escaping () throws -> T) -> (Bool) throws -> T { 61 | var cache: T? 62 | return { withoutCaching in 63 | if withoutCaching || cache == nil { 64 | cache = try closure() 65 | } 66 | guard let cache = cache else { 67 | preconditionFailure() 68 | } 69 | 70 | return cache 71 | } 72 | } 73 | 74 | let runLoop = RunLoop.current 75 | let timeoutDate = Date(timeInterval: TimeInterval(timeout), since: Date()) 76 | var fulfilled = false 77 | let _expression = memoizedClosure(expression) 78 | repeat { 79 | do { 80 | let exp = try _expression(true) 81 | fulfilled = predicate(exp) 82 | } catch { 83 | fatalError("Error raised \(error.localizedDescription)") 84 | } 85 | if !fulfilled { 86 | runLoop.run(until: Date(timeIntervalSinceNow: pollingInterval.timeInterval)) 87 | } else { 88 | break 89 | } 90 | } while Date().compare(timeoutDate) == .orderedAscending 91 | 92 | if fulfilled { 93 | return .success 94 | } else { 95 | return .timedOut 96 | } 97 | } 98 | } 99 | 100 | // Handle threading stuff 101 | extension Runner { 102 | func startSocketThread(port: UInt32) { 103 | let secondsToWait = DispatchTimeInterval.seconds(SocketClient.connectTimeoutSeconds) 104 | 105 | dispatchGroup.enter() 106 | 107 | socketClient = SocketClient(port: port, commandTimeoutSeconds: timeout, socketDelegate: self) 108 | thread = Thread(target: self, selector: #selector(startSocketComs), object: nil) 109 | guard let thread = thread else { 110 | preconditionFailure("Thread did not instantiate correctly") 111 | } 112 | 113 | thread.name = "socket thread" 114 | thread.start() 115 | 116 | let connectTimeout = DispatchTime.now() + secondsToWait 117 | let timeoutResult = dispatchGroup.wait(timeout: connectTimeout) 118 | 119 | let failureMessage = "couldn't start socket thread in: \(SocketClient.connectTimeoutSeconds) seconds" 120 | let success = testDispatchTimeoutResult(timeoutResult, failureMessage: failureMessage, timeToWait: secondsToWait) 121 | guard success else { 122 | log(message: "socket thread timeout") 123 | preconditionFailure() 124 | } 125 | } 126 | 127 | func disconnectFromFastlaneProcess() { 128 | shouldLeaveDispatchGroupDuringDisconnect = true 129 | dispatchGroup.enter() 130 | socketClient.sendComplete() 131 | 132 | let connectTimeout = DispatchTime.now() + 2 133 | _ = dispatchGroup.wait(timeout: connectTimeout) 134 | } 135 | 136 | @objc func startSocketComs() { 137 | guard let socketClient = socketClient else { 138 | return 139 | } 140 | 141 | socketClient.connectAndOpenStreams() 142 | dispatchGroup.leave() 143 | } 144 | 145 | private func testDispatchTimeoutResult(_ timeoutResult: DispatchTimeoutResult, failureMessage: String, timeToWait _: DispatchTimeInterval) -> Bool { 146 | switch timeoutResult { 147 | case .success: 148 | return true 149 | case .timedOut: 150 | log(message: "timeout: \(failureMessage)") 151 | return false 152 | } 153 | } 154 | } 155 | 156 | extension Runner: SocketClientDelegateProtocol { 157 | func commandExecuted(serverResponse: SocketClientResponse, completion: (SocketClient) -> Void) { 158 | switch serverResponse { 159 | case let .success(returnedObject, closureArgumentValue): 160 | verbose(message: "command executed") 161 | returnValue = returnedObject 162 | if let command = currentlyExecutingCommand as? RubyCommand { 163 | if let closureArgumentValue = closureArgumentValue, !closureArgumentValue.isEmpty { 164 | command.performCallback(callbackArg: closureArgumentValue, socket: socketClient) { 165 | self.executeNext[command.id] = true 166 | } 167 | } else { 168 | executeNext[command.id] = true 169 | } 170 | } 171 | dispatchGroup.leave() 172 | completion(socketClient) 173 | case .clientInitiatedCancelAcknowledged: 174 | verbose(message: "server acknowledged a cancel request") 175 | dispatchGroup.leave() 176 | if let command = currentlyExecutingCommand as? RubyCommand { 177 | executeNext[command.id] = true 178 | } 179 | completion(socketClient) 180 | case .alreadyClosedSockets, .connectionFailure, .malformedRequest, .malformedResponse, .serverError: 181 | log(message: "error encountered while executing command:\n\(serverResponse)") 182 | dispatchGroup.leave() 183 | if let command = currentlyExecutingCommand as? RubyCommand { 184 | executeNext[command.id] = true 185 | } 186 | completion(socketClient) 187 | case let .commandTimeout(timeout): 188 | log(message: "Runner timed out after \(timeout) second(s)") 189 | } 190 | } 191 | 192 | func connectionsOpened() { 193 | DispatchQueue.main.async { 194 | verbose(message: "connected!") 195 | } 196 | } 197 | 198 | func connectionsClosed() { 199 | DispatchQueue.main.async { 200 | if let thread = self.thread { 201 | thread.cancel() 202 | } 203 | self.thread = nil 204 | self.socketClient.closeSession() 205 | self.socketClient = nil 206 | verbose(message: "connection closed!") 207 | if self.shouldLeaveDispatchGroupDuringDisconnect { 208 | self.dispatchGroup.leave() 209 | } 210 | exit(0) 211 | } 212 | } 213 | } 214 | 215 | class Logger { 216 | enum LogMode { 217 | init(logMode: String) { 218 | switch logMode { 219 | case "normal", "default": 220 | self = .normal 221 | case "verbose": 222 | self = .verbose 223 | default: 224 | logger.log(message: "unrecognized log mode: \(logMode), defaulting to 'normal'") 225 | self = .normal 226 | } 227 | } 228 | 229 | case normal 230 | case verbose 231 | } 232 | 233 | public static var logMode: LogMode = .normal 234 | 235 | func log(message: String) { 236 | let timestamp = NSDate().timeIntervalSince1970 237 | print("[\(timestamp)]: \(message)") 238 | } 239 | 240 | func verbose(message: String) { 241 | if Logger.logMode == .verbose { 242 | let timestamp = NSDate().timeIntervalSince1970 243 | print("[\(timestamp)]: \(message)") 244 | } 245 | } 246 | } 247 | 248 | func log(message: String) { 249 | logger.log(message: message) 250 | } 251 | 252 | func verbose(message: String) { 253 | logger.verbose(message: message) 254 | } 255 | 256 | private extension DispatchTimeInterval { 257 | var timeInterval: TimeInterval { 258 | var result: TimeInterval = 0 259 | switch self { 260 | case let .seconds(value): 261 | result = TimeInterval(value) 262 | case let .milliseconds(value): 263 | result = TimeInterval(value) * 0.001 264 | case let .microseconds(value): 265 | result = TimeInterval(value) * 0.000_001 266 | case let .nanoseconds(value): 267 | result = TimeInterval(value) * 0.000_000_001 268 | case .never: 269 | fatalError() 270 | @unknown default: 271 | fatalError() 272 | } 273 | return result 274 | } 275 | } 276 | 277 | // Please don't remove the lines below 278 | // They are used to detect outdated files 279 | // FastlaneRunnerAPIVersion [0.9.2] 280 | -------------------------------------------------------------------------------- /fastlane/swift/RunnerArgument.swift: -------------------------------------------------------------------------------- 1 | // RunnerArgument.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | // 5 | // ** NOTE ** 6 | // This file is provided by fastlane and WILL be overwritten in future updates 7 | // If you want to add extra functionality to this project, create a new file in a 8 | // new group so that it won't be marked for upgrade 9 | // 10 | 11 | import Foundation 12 | 13 | struct RunnerArgument { 14 | let name: String 15 | let value: String 16 | } 17 | 18 | // Please don't remove the lines below 19 | // They are used to detect outdated files 20 | // FastlaneRunnerAPIVersion [0.9.2] 21 | -------------------------------------------------------------------------------- /fastlane/swift/Scanfile.swift: -------------------------------------------------------------------------------- 1 | // Scanfile.swift 2 | // Copyright (c) 2021 FastlaneTools 3 | 4 | // This class is automatically included in FastlaneRunner during build 5 | 6 | // This autogenerated file will be overwritten or replaced during build time, or when you initialize `scan` 7 | // 8 | // ** NOTE ** 9 | // This file is provided by fastlane and WILL be overwritten in future updates 10 | // If you want to add extra functionality to this project, create a new file in a 11 | // new group so that it won't be marked for upgrade 12 | // 13 | 14 | public class Scanfile: ScanfileProtocol { 15 | // If you want to enable `scan`, run `fastlane scan init` 16 | // After, this file will be replaced with a custom implementation that contains values you supplied 17 | // during the `init` process, and you won't see this message 18 | } 19 | 20 | // Generated with fastlane 2.195.0 21 | -------------------------------------------------------------------------------- /fastlane/swift/Screengrabfile.swift: -------------------------------------------------------------------------------- 1 | // Screengrabfile.swift 2 | // Copyright (c) 2021 FastlaneTools 3 | 4 | // This class is automatically included in FastlaneRunner during build 5 | 6 | // This autogenerated file will be overwritten or replaced during build time, or when you initialize `screengrab` 7 | // 8 | // ** NOTE ** 9 | // This file is provided by fastlane and WILL be overwritten in future updates 10 | // If you want to add extra functionality to this project, create a new file in a 11 | // new group so that it won't be marked for upgrade 12 | // 13 | 14 | public class Screengrabfile: ScreengrabfileProtocol { 15 | // If you want to enable `screengrab`, run `fastlane screengrab init` 16 | // After, this file will be replaced with a custom implementation that contains values you supplied 17 | // during the `init` process, and you won't see this message 18 | } 19 | 20 | // Generated with fastlane 2.195.0 21 | -------------------------------------------------------------------------------- /fastlane/swift/ScreengrabfileProtocol.swift: -------------------------------------------------------------------------------- 1 | // ScreengrabfileProtocol.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | public protocol ScreengrabfileProtocol: AnyObject { 5 | /// Path to the root of your Android SDK installation, e.g. ~/tools/android-sdk-macosx 6 | var androidHome: String? { get } 7 | 8 | /// **DEPRECATED!** The Android build tools version to use, e.g. '23.0.2' 9 | var buildToolsVersion: String? { get } 10 | 11 | /// A list of locales which should be used 12 | var locales: [String] { get } 13 | 14 | /// Enabling this option will automatically clear previously generated screenshots before running screengrab 15 | var clearPreviousScreenshots: Bool { get } 16 | 17 | /// The directory where to store the screenshots 18 | var outputDirectory: String { get } 19 | 20 | /// Don't open the summary after running _screengrab_ 21 | var skipOpenSummary: Bool { get } 22 | 23 | /// The package name of the app under test (e.g. com.yourcompany.yourapp) 24 | var appPackageName: String { get } 25 | 26 | /// The package name of the tests bundle (e.g. com.yourcompany.yourapp.test) 27 | var testsPackageName: String? { get } 28 | 29 | /// Only run tests in these Java packages 30 | var useTestsInPackages: [String]? { get } 31 | 32 | /// Only run tests in these Java classes 33 | var useTestsInClasses: [String]? { get } 34 | 35 | /// Additional launch arguments 36 | var launchArguments: [String]? { get } 37 | 38 | /// The fully qualified class name of your test instrumentation runner 39 | var testInstrumentationRunner: String { get } 40 | 41 | /// **DEPRECATED!** Return the device to this locale after running tests 42 | var endingLocale: String { get } 43 | 44 | /// **DEPRECATED!** Restarts the adb daemon using `adb root` to allow access to screenshots directories on device. Use if getting 'Permission denied' errors 45 | var useAdbRoot: Bool { get } 46 | 47 | /// The path to the APK for the app under test 48 | var appApkPath: String? { get } 49 | 50 | /// The path to the APK for the tests bundle 51 | var testsApkPath: String? { get } 52 | 53 | /// Use the device or emulator with the given serial number or qualifier 54 | var specificDevice: String? { get } 55 | 56 | /// Type of device used for screenshots. Matches Google Play Types (phone, sevenInch, tenInch, tv, wear) 57 | var deviceType: String { get } 58 | 59 | /// Whether or not to exit Screengrab on test failure. Exiting on failure will not copy screenshots to local machine nor open screenshots summary 60 | var exitOnTestFailure: Bool { get } 61 | 62 | /// Enabling this option will automatically uninstall the application before running it 63 | var reinstallApp: Bool { get } 64 | 65 | /// Add timestamp suffix to screenshot filename 66 | var useTimestampSuffix: Bool { get } 67 | 68 | /// Configure the host used by adb to connect, allows running on remote devices farm 69 | var adbHost: String? { get } 70 | } 71 | 72 | public extension ScreengrabfileProtocol { 73 | var androidHome: String? { return nil } 74 | var buildToolsVersion: String? { return nil } 75 | var locales: [String] { return ["en-US"] } 76 | var clearPreviousScreenshots: Bool { return false } 77 | var outputDirectory: String { return "fastlane/metadata/android" } 78 | var skipOpenSummary: Bool { return false } 79 | var appPackageName: String { return "" } 80 | var testsPackageName: String? { return nil } 81 | var useTestsInPackages: [String]? { return nil } 82 | var useTestsInClasses: [String]? { return nil } 83 | var launchArguments: [String]? { return nil } 84 | var testInstrumentationRunner: String { return "androidx.test.runner.AndroidJUnitRunner" } 85 | var endingLocale: String { return "en-US" } 86 | var useAdbRoot: Bool { return false } 87 | var appApkPath: String? { return nil } 88 | var testsApkPath: String? { return nil } 89 | var specificDevice: String? { return nil } 90 | var deviceType: String { return "phone" } 91 | var exitOnTestFailure: Bool { return true } 92 | var reinstallApp: Bool { return false } 93 | var useTimestampSuffix: Bool { return true } 94 | var adbHost: String? { return nil } 95 | } 96 | 97 | // Please don't remove the lines below 98 | // They are used to detect outdated files 99 | // FastlaneRunnerAPIVersion [0.9.105] 100 | -------------------------------------------------------------------------------- /fastlane/swift/Snapshotfile.swift: -------------------------------------------------------------------------------- 1 | // Snapshotfile.swift 2 | // Copyright (c) 2021 FastlaneTools 3 | 4 | // This class is automatically included in FastlaneRunner during build 5 | 6 | // This autogenerated file will be overwritten or replaced during build time, or when you initialize `snapshot` 7 | // 8 | // ** NOTE ** 9 | // This file is provided by fastlane and WILL be overwritten in future updates 10 | // If you want to add extra functionality to this project, create a new file in a 11 | // new group so that it won't be marked for upgrade 12 | // 13 | 14 | public class Snapshotfile: SnapshotfileProtocol { 15 | // If you want to enable `snapshot`, run `fastlane snapshot init` 16 | // After, this file will be replaced with a custom implementation that contains values you supplied 17 | // during the `init` process, and you won't see this message 18 | } 19 | 20 | // Generated with fastlane 2.195.0 21 | -------------------------------------------------------------------------------- /fastlane/swift/SnapshotfileProtocol.swift: -------------------------------------------------------------------------------- 1 | // SnapshotfileProtocol.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | public protocol SnapshotfileProtocol: AnyObject { 5 | /// Path the workspace file 6 | var workspace: String? { get } 7 | 8 | /// Path the project file 9 | var project: String? { get } 10 | 11 | /// Pass additional arguments to xcodebuild for the test phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" 12 | var xcargs: String? { get } 13 | 14 | /// Use an extra XCCONFIG file to build your app 15 | var xcconfig: String? { get } 16 | 17 | /// A list of devices you want to take the screenshots from 18 | var devices: [String]? { get } 19 | 20 | /// A list of languages which should be used 21 | var languages: [String] { get } 22 | 23 | /// A list of launch arguments which should be used 24 | var launchArguments: [String] { get } 25 | 26 | /// The directory where to store the screenshots 27 | var outputDirectory: String { get } 28 | 29 | /// If the logs generated by the app (e.g. using NSLog, perror, etc.) in the Simulator should be written to the output_directory 30 | var outputSimulatorLogs: Bool { get } 31 | 32 | /// By default, the latest version should be used automatically. If you want to change it, do it here 33 | var iosVersion: String? { get } 34 | 35 | /// Don't open the HTML summary after running _snapshot_ 36 | var skipOpenSummary: Bool { get } 37 | 38 | /// Do not check for most recent SnapshotHelper code 39 | var skipHelperVersionCheck: Bool { get } 40 | 41 | /// Enabling this option will automatically clear previously generated screenshots before running snapshot 42 | var clearPreviousScreenshots: Bool { get } 43 | 44 | /// Enabling this option will automatically uninstall the application before running it 45 | var reinstallApp: Bool { get } 46 | 47 | /// Enabling this option will automatically erase the simulator before running the application 48 | var eraseSimulator: Bool { get } 49 | 50 | /// Enabling this option will prevent displaying the simulator window 51 | var headless: Bool { get } 52 | 53 | /// Enabling this option will automatically override the status bar to show 9:41 AM, full battery, and full reception (Adjust 'SNAPSHOT_SIMULATOR_WAIT_FOR_BOOT_TIMEOUT' environment variable if override status bar is not working. Might be because simulator is not fully booted. Defaults to 10 seconds) 54 | var overrideStatusBar: Bool { get } 55 | 56 | /// Fully customize the status bar by setting each option here. Requires `override_status_bar` to be set to `true`. See `xcrun simctl status_bar --help` 57 | var overrideStatusBarArguments: String? { get } 58 | 59 | /// Enabling this option will configure the Simulator's system language 60 | var localizeSimulator: Bool { get } 61 | 62 | /// Enabling this option will configure the Simulator to be in dark mode (false for light, true for dark) 63 | var darkMode: Bool? { get } 64 | 65 | /// The bundle identifier of the app to uninstall (only needed when enabling reinstall_app) 66 | var appIdentifier: String? { get } 67 | 68 | /// A list of photos that should be added to the simulator before running the application 69 | var addPhotos: [String]? { get } 70 | 71 | /// A list of videos that should be added to the simulator before running the application 72 | var addVideos: [String]? { get } 73 | 74 | /// A path to screenshots.html template 75 | var htmlTemplate: String? { get } 76 | 77 | /// The directory where to store the build log 78 | var buildlogPath: String { get } 79 | 80 | /// Should the project be cleaned before building it? 81 | var clean: Bool { get } 82 | 83 | /// Test without building, requires a derived data path 84 | var testWithoutBuilding: Bool? { get } 85 | 86 | /// The configuration to use when building the app. Defaults to 'Release' 87 | var configuration: String? { get } 88 | 89 | /// The SDK that should be used for building the application 90 | var sdk: String? { get } 91 | 92 | /// The scheme you want to use, this must be the scheme for the UI Tests 93 | var scheme: String? { get } 94 | 95 | /// The number of times a test can fail before snapshot should stop retrying 96 | var numberOfRetries: Int { get } 97 | 98 | /// Should snapshot stop immediately after the tests completely failed on one device? 99 | var stopAfterFirstError: Bool { get } 100 | 101 | /// The directory where build products and other derived data will go 102 | var derivedDataPath: String? { get } 103 | 104 | /// Should an Xcode result bundle be generated in the output directory 105 | var resultBundle: Bool { get } 106 | 107 | /// The name of the target you want to test (if you desire to override the Target Application from Xcode) 108 | var testTargetName: String? { get } 109 | 110 | /// Separate the log files per device and per language 111 | var namespaceLogFiles: String? { get } 112 | 113 | /// Take snapshots on multiple simulators concurrently. Note: This option is only applicable when running against Xcode 9 114 | var concurrentSimulators: Bool { get } 115 | 116 | /// Disable the simulator from showing the 'Slide to type' prompt 117 | var disableSlideToType: Bool { get } 118 | 119 | /// Sets a custom path for Swift Package Manager dependencies 120 | var clonedSourcePackagesPath: String? { get } 121 | 122 | /// Skips resolution of Swift Package Manager dependencies 123 | var skipPackageDependenciesResolution: Bool { get } 124 | 125 | /// Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file 126 | var disablePackageAutomaticUpdates: Bool { get } 127 | 128 | /// The testplan associated with the scheme that should be used for testing 129 | var testplan: String? { get } 130 | 131 | /// Array of strings matching Test Bundle/Test Suite/Test Cases to run 132 | var onlyTesting: String? { get } 133 | 134 | /// Array of strings matching Test Bundle/Test Suite/Test Cases to skip 135 | var skipTesting: String? { get } 136 | 137 | /// xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) 138 | var xcodebuildFormatter: String { get } 139 | 140 | /// **DEPRECATED!** Use `xcodebuild_formatter: ''` instead - Additional xcpretty arguments 141 | var xcprettyArgs: String? { get } 142 | 143 | /// Disable xcpretty formatting of build 144 | var disableXcpretty: Bool? { get } 145 | 146 | /// Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path 147 | var suppressXcodeOutput: Bool? { get } 148 | 149 | /// Lets xcodebuild use system's scm configuration 150 | var useSystemScm: Bool { get } 151 | } 152 | 153 | public extension SnapshotfileProtocol { 154 | var workspace: String? { return nil } 155 | var project: String? { return nil } 156 | var xcargs: String? { return nil } 157 | var xcconfig: String? { return nil } 158 | var devices: [String]? { return nil } 159 | var languages: [String] { return ["en-US"] } 160 | var launchArguments: [String] { return [""] } 161 | var outputDirectory: String { return "screenshots" } 162 | var outputSimulatorLogs: Bool { return false } 163 | var iosVersion: String? { return nil } 164 | var skipOpenSummary: Bool { return false } 165 | var skipHelperVersionCheck: Bool { return false } 166 | var clearPreviousScreenshots: Bool { return false } 167 | var reinstallApp: Bool { return false } 168 | var eraseSimulator: Bool { return false } 169 | var headless: Bool { return true } 170 | var overrideStatusBar: Bool { return false } 171 | var overrideStatusBarArguments: String? { return nil } 172 | var localizeSimulator: Bool { return false } 173 | var darkMode: Bool? { return nil } 174 | var appIdentifier: String? { return nil } 175 | var addPhotos: [String]? { return nil } 176 | var addVideos: [String]? { return nil } 177 | var htmlTemplate: String? { return nil } 178 | var buildlogPath: String { return "~/Library/Logs/snapshot" } 179 | var clean: Bool { return false } 180 | var testWithoutBuilding: Bool? { return nil } 181 | var configuration: String? { return nil } 182 | var sdk: String? { return nil } 183 | var scheme: String? { return nil } 184 | var numberOfRetries: Int { return 1 } 185 | var stopAfterFirstError: Bool { return false } 186 | var derivedDataPath: String? { return nil } 187 | var resultBundle: Bool { return false } 188 | var testTargetName: String? { return nil } 189 | var namespaceLogFiles: String? { return nil } 190 | var concurrentSimulators: Bool { return true } 191 | var disableSlideToType: Bool { return false } 192 | var clonedSourcePackagesPath: String? { return nil } 193 | var skipPackageDependenciesResolution: Bool { return false } 194 | var disablePackageAutomaticUpdates: Bool { return false } 195 | var testplan: String? { return nil } 196 | var onlyTesting: String? { return nil } 197 | var skipTesting: String? { return nil } 198 | var xcodebuildFormatter: String { return "xcbeautify" } 199 | var xcprettyArgs: String? { return nil } 200 | var disableXcpretty: Bool? { return nil } 201 | var suppressXcodeOutput: Bool? { return nil } 202 | var useSystemScm: Bool { return false } 203 | } 204 | 205 | // Please don't remove the lines below 206 | // They are used to detect outdated files 207 | // FastlaneRunnerAPIVersion [0.9.99] 208 | -------------------------------------------------------------------------------- /fastlane/swift/SocketClientDelegateProtocol.swift: -------------------------------------------------------------------------------- 1 | // SocketClientDelegateProtocol.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | // 5 | // ** NOTE ** 6 | // This file is provided by fastlane and WILL be overwritten in future updates 7 | // If you want to add extra functionality to this project, create a new file in a 8 | // new group so that it won't be marked for upgrade 9 | // 10 | 11 | import Foundation 12 | 13 | protocol SocketClientDelegateProtocol: AnyObject { 14 | func connectionsOpened() 15 | func connectionsClosed() 16 | func commandExecuted(serverResponse: SocketClientResponse, completion: (SocketClient) -> Void) 17 | } 18 | 19 | // Please don't remove the lines below 20 | // They are used to detect outdated files 21 | // FastlaneRunnerAPIVersion [0.9.2] 22 | -------------------------------------------------------------------------------- /fastlane/swift/SocketResponse.swift: -------------------------------------------------------------------------------- 1 | // SocketResponse.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | // 5 | // ** NOTE ** 6 | // This file is provided by fastlane and WILL be overwritten in future updates 7 | // If you want to add extra functionality to this project, create a new file in a 8 | // new group so that it won't be marked for upgrade 9 | // 10 | 11 | import Foundation 12 | 13 | struct SocketResponse { 14 | enum ResponseType { 15 | case parseFailure(failureInformation: [String]) 16 | case failure(failureInformation: [String], failureClass: String?, failureMessage: String?) 17 | case readyForNext(returnedObject: String?, closureArgumentValue: String?) 18 | case clientInitiatedCancel 19 | 20 | init(statusDictionary: [String: Any]) { 21 | guard let status = statusDictionary["status"] as? String else { 22 | self = .parseFailure(failureInformation: ["Message failed to parse from Ruby server"]) 23 | return 24 | } 25 | 26 | if status == "ready_for_next" { 27 | verbose(message: "ready for next") 28 | let returnedObject = statusDictionary["return_object"] as? String 29 | let closureArgumentValue = statusDictionary["closure_argument_value"] as? String 30 | self = .readyForNext(returnedObject: returnedObject, closureArgumentValue: closureArgumentValue) 31 | return 32 | 33 | } else if status == "cancelled" { 34 | self = .clientInitiatedCancel 35 | return 36 | 37 | } else if status == "failure" { 38 | guard let failureInformation = statusDictionary["failure_information"] as? [String] else { 39 | self = .parseFailure(failureInformation: ["Ruby server indicated failure but Swift couldn't receive it"]) 40 | return 41 | } 42 | 43 | let failureClass = statusDictionary["failure_class"] as? String 44 | let failureMessage = statusDictionary["failure_message"] as? String 45 | self = .failure(failureInformation: failureInformation, failureClass: failureClass, failureMessage: failureMessage) 46 | return 47 | } 48 | self = .parseFailure(failureInformation: ["Message status: \(status) not a supported status"]) 49 | } 50 | } 51 | 52 | let responseType: ResponseType 53 | 54 | init(payload: String) { 55 | guard let data = SocketResponse.convertToDictionary(text: payload) else { 56 | responseType = .parseFailure(failureInformation: ["Unable to parse message from Ruby server"]) 57 | return 58 | } 59 | 60 | guard case let statusDictionary? = data["payload"] as? [String: Any] else { 61 | responseType = .parseFailure(failureInformation: ["Payload missing from Ruby server response"]) 62 | return 63 | } 64 | 65 | responseType = ResponseType(statusDictionary: statusDictionary) 66 | } 67 | } 68 | 69 | extension SocketResponse { 70 | static func convertToDictionary(text: String) -> [String: Any]? { 71 | if let data = text.data(using: .utf8) { 72 | do { 73 | return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] 74 | } catch { 75 | log(message: error.localizedDescription) 76 | } 77 | } 78 | return nil 79 | } 80 | } 81 | 82 | // Please don't remove the lines below 83 | // They are used to detect outdated files 84 | // FastlaneRunnerAPIVersion [0.9.2] 85 | -------------------------------------------------------------------------------- /fastlane/swift/formatting/Brewfile: -------------------------------------------------------------------------------- 1 | brew("swiftformat") 2 | -------------------------------------------------------------------------------- /fastlane/swift/formatting/Brewfile.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": { 3 | "brew": { 4 | "swiftformat": { 5 | "version": "0.48.11", 6 | "bottle": { 7 | "rebuild": 0, 8 | "root_url": "https://ghcr.io/v2/homebrew/core", 9 | "files": { 10 | "arm64_big_sur": { 11 | "cellar": ":any_skip_relocation", 12 | "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:e0a851cfa2ff5d04f0fc98a9e624d1411f1b5b1e55e3cbc0901f4913c02e716a", 13 | "sha256": "e0a851cfa2ff5d04f0fc98a9e624d1411f1b5b1e55e3cbc0901f4913c02e716a" 14 | }, 15 | "big_sur": { 16 | "cellar": ":any_skip_relocation", 17 | "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:a5327283fe32b2ef2c6f264e14c966a9a60cb291415d3d05ed659c92a93c4987", 18 | "sha256": "a5327283fe32b2ef2c6f264e14c966a9a60cb291415d3d05ed659c92a93c4987" 19 | }, 20 | "catalina": { 21 | "cellar": ":any_skip_relocation", 22 | "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:ba95e49ecc71bb19734698dee565e3b0ced6470729206cb434675cfa051f2755", 23 | "sha256": "ba95e49ecc71bb19734698dee565e3b0ced6470729206cb434675cfa051f2755" 24 | }, 25 | "mojave": { 26 | "cellar": ":any_skip_relocation", 27 | "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:c7e00eae9d46dddf040999f0f2832d08110f093c7a403aaaaaa18d8830213967", 28 | "sha256": "c7e00eae9d46dddf040999f0f2832d08110f093c7a403aaaaaa18d8830213967" 29 | } 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | "system": { 36 | "macos": { 37 | "catalina": { 38 | "HOMEBREW_VERSION": "3.2.0-77-gd305f72", 39 | "HOMEBREW_PREFIX": "/usr/local", 40 | "Homebrew/homebrew-core": "0b13b342053d414d1b241c2c7a446b74d79cc90e", 41 | "CLT": "11.0.0.33.12", 42 | "Xcode": "12.4", 43 | "macOS": "10.15.7" 44 | }, 45 | "big_sur": { 46 | "HOMEBREW_VERSION": "3.2.10-50-ge3f851d", 47 | "HOMEBREW_PREFIX": "/opt/homebrew", 48 | "Homebrew/homebrew-core": "73588fb5f5edccfe62f1b290a3298b402fbd71d5", 49 | "CLT": "12.5.1.0.1.1623191612", 50 | "Xcode": "13.0", 51 | "macOS": "11.5.2" 52 | }, 53 | "monterey": { 54 | "HOMEBREW_VERSION": "3.2.13-55-ga6959e4", 55 | "HOMEBREW_PREFIX": "/usr/local", 56 | "Homebrew/homebrew-core": "3fb109275770551bba03c7055d75ceec2c38b1b2", 57 | "CLT": "13.0.0.0.1.1628499445", 58 | "Xcode": "13.0", 59 | "macOS": "12.0" 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /fastlane/swift/formatting/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | task(default: %w[setup]) 4 | 5 | task(setup: [:brew, :lint]) 6 | 7 | task(:brew) do 8 | raise '`brew` is required. Please install brew. https://brew.sh/' unless system('which brew') 9 | 10 | puts('➡️ Brew') 11 | sh('brew bundle') 12 | end 13 | 14 | task(:lint) do 15 | Dir.chdir('..') do 16 | sh("swiftformat . --config formatting/.swiftformat --verbose --selfrequired waitWithPolling --exclude Fastfile.swift --swiftversion 4.0") 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /fastlane/swift/main.swift: -------------------------------------------------------------------------------- 1 | // main.swift 2 | // Copyright (c) 2022 FastlaneTools 3 | 4 | // 5 | // ** NOTE ** 6 | // This file is provided by fastlane and WILL be overwritten in future updates 7 | // If you want to add extra functionality to this project, create a new file in a 8 | // new group so that it won't be marked for upgrade 9 | // 10 | 11 | import Foundation 12 | 13 | let argumentProcessor = ArgumentProcessor(args: CommandLine.arguments) 14 | let timeout = argumentProcessor.commandTimeout 15 | 16 | class MainProcess { 17 | var doneRunningLane = false 18 | var thread: Thread! 19 | 20 | @objc func connectToFastlaneAndRunLane() { 21 | runner.startSocketThread(port: argumentProcessor.port) 22 | 23 | let completedRun = Fastfile.runLane(from: nil, named: argumentProcessor.currentLane, with: argumentProcessor.laneParameters()) 24 | if completedRun { 25 | runner.disconnectFromFastlaneProcess() 26 | } 27 | 28 | doneRunningLane = true 29 | } 30 | 31 | func startFastlaneThread() { 32 | thread = Thread(target: self, selector: #selector(connectToFastlaneAndRunLane), object: nil) 33 | thread.name = "worker thread" 34 | thread.start() 35 | } 36 | } 37 | 38 | let process = MainProcess() 39 | process.startFastlaneThread() 40 | 41 | while !process.doneRunningLane, RunLoop.current.run(mode: RunLoopMode.defaultRunLoopMode, before: Date(timeIntervalSinceNow: 2)) { 42 | // no op 43 | } 44 | 45 | // Please don't remove the lines below 46 | // They are used to detect outdated files 47 | // FastlaneRunnerAPIVersion [0.9.2] 48 | -------------------------------------------------------------------------------- /fastlane/swift/upgrade_manifest.json: -------------------------------------------------------------------------------- 1 | {"Actions.swift":"Autogenerated API","Fastlane.swift":"Autogenerated API","DeliverfileProtocol.swift":"Autogenerated API","GymfileProtocol.swift":"Autogenerated API","MatchfileProtocol.swift":"Autogenerated API","Plugins.swift":"Autogenerated API","PrecheckfileProtocol.swift":"Autogenerated API","ScanfileProtocol.swift":"Autogenerated API","ScreengrabfileProtocol.swift":"Autogenerated API","SnapshotfileProtocol.swift":"Autogenerated API","LaneFileProtocol.swift":"Fastfile Components","OptionalConfigValue.swift":"Fastfile Components","ControlCommand.swift":"Networking","RubyCommand.swift":"Networking","RubyCommandable.swift":"Networking","Runner.swift":"Networking","SocketClient.swift":"Networking","SocketClientDelegateProtocol.swift":"Networking","SocketResponse.swift":"Networking","main.swift":"Runner Code","ArgumentProcessor.swift":"Runner Code","RunnerArgument.swift":"Runner Code"} --------------------------------------------------------------------------------