├── .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 |
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"}
--------------------------------------------------------------------------------