Compare commits
271 Commits
0.1.1
...
fabianthde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2939919ddb | ||
|
|
38a1c7eef6 | ||
|
|
f6252c3a8b | ||
|
|
653d80b88e | ||
|
|
89609ad35c | ||
|
|
2211013e57 | ||
|
|
f206ee1406 | ||
|
|
00dc9b36af | ||
|
|
24146cef90 | ||
|
|
c46a50ec58 | ||
|
|
de7e909c01 | ||
|
|
fbc754d8b7 | ||
|
|
767d878051 | ||
|
|
132b140af2 | ||
|
|
df7d8871ff | ||
|
|
ca2398e4c7 | ||
|
|
b8f02d2152 | ||
|
|
e85876cd24 | ||
|
|
3f06a53058 | ||
|
|
4ee053a4f9 | ||
|
|
e5369524ce | ||
|
|
77465cebd0 | ||
|
|
f90bf3bfcf | ||
|
|
0000610b9d | ||
|
|
c7e095583d | ||
|
|
a725f3e9cc | ||
|
|
b5dea18073 | ||
|
|
b9b309e603 | ||
|
|
15f1be0aa8 | ||
|
|
ffd80ce0b4 | ||
|
|
350891ee2a | ||
|
|
5dec1cd561 | ||
|
|
c4d235d742 | ||
|
|
cdc6675dd5 | ||
|
|
85635bb26e | ||
|
|
3be0a4a89c | ||
|
|
47e47fb3cf | ||
|
|
48903034b6 | ||
|
|
6952218ee7 | ||
|
|
80146c1e03 | ||
|
|
642ae996c9 | ||
|
|
8040636aa5 | ||
|
|
731fcfaca7 | ||
|
|
708fb3fccd | ||
|
|
9f429fb068 | ||
|
|
29fc693f4d | ||
|
|
6f373ad305 | ||
|
|
c069d779d9 | ||
|
|
cd88970a22 | ||
|
|
6b6708e43c | ||
|
|
9206eeb9e3 | ||
|
|
080bbb3c51 | ||
|
|
ea2c862900 | ||
|
|
4fe72ea113 | ||
|
|
c486a62b50 | ||
|
|
3ce4451da4 | ||
|
|
294ba12391 | ||
|
|
4a3343fe61 | ||
|
|
d1e6ddd435 | ||
|
|
3e0379dc70 | ||
|
|
d99674f8bd | ||
|
|
ca7acc17da | ||
|
|
16a8bce102 | ||
|
|
ed2270ff46 | ||
|
|
45b6c3b338 | ||
|
|
84e2284f56 | ||
|
|
1c0d0be622 | ||
|
|
a9ce0f487d | ||
|
|
07533e0365 | ||
|
|
ee5ddd4264 | ||
|
|
f519d22d81 | ||
|
|
51ed87086a | ||
|
|
1ca3aa3cdb | ||
|
|
0178c63f6a | ||
|
|
8a97c409fa | ||
|
|
3dd0735305 | ||
|
|
536f775baa | ||
|
|
00f7a684a3 | ||
|
|
d79b166a6a | ||
|
|
b3d827f56a | ||
|
|
40bcef1dcb | ||
|
|
6146f1bdaa | ||
|
|
f5d82d9ef0 | ||
|
|
b2a29ae606 | ||
|
|
98ccba53a2 | ||
|
|
9bfda36647 | ||
|
|
5710cdf19c | ||
|
|
20cf54bfcd | ||
|
|
2ce639e750 | ||
|
|
b1ed413c4f | ||
|
|
b8c3060037 | ||
|
|
c3ea4940d7 | ||
|
|
40e1225b87 | ||
|
|
0c171122b2 | ||
|
|
6d0f4bb3da | ||
|
|
5e2cc6e20c | ||
|
|
99cb43bbea | ||
|
|
ca7d8277f7 | ||
|
|
337d26333e | ||
|
|
ebb64d255b | ||
|
|
7dcb199f68 | ||
|
|
4334e887de | ||
|
|
4e84dc4cc8 | ||
|
|
1a1ed072bf | ||
|
|
ae457f07c4 | ||
|
|
00095942c3 | ||
|
|
d1caa5fc21 | ||
|
|
813e2f97ac | ||
|
|
bcb5a90f5e | ||
|
|
020a1a3149 | ||
|
|
c4d649ec58 | ||
|
|
c02cf2c284 | ||
|
|
c30afd042e | ||
|
|
17640fe6cf | ||
|
|
2e4f6ee420 | ||
|
|
a3768d9221 | ||
|
|
80c3390363 | ||
|
|
a5e3869d8f | ||
|
|
aa7d7c2d02 | ||
|
|
015f205569 | ||
|
|
e59fb15926 | ||
|
|
173c585f2d | ||
|
|
6f8c27793e | ||
|
|
332b81c803 | ||
|
|
4b343b500d | ||
|
|
e87c537642 | ||
|
|
2e6300cce2 | ||
|
|
09514d15a6 | ||
|
|
0de23dcba0 | ||
|
|
bacb153151 | ||
|
|
a01aa299d8 | ||
|
|
44edbddbd8 | ||
|
|
79d677cf3c | ||
|
|
be39b6512f | ||
|
|
fcfeea35da | ||
|
|
7d0eb8c61e | ||
|
|
4d8438a6b6 | ||
|
|
f611244e35 | ||
|
|
546a978d3b | ||
|
|
70b23fb073 | ||
|
|
a56ca597d6 | ||
|
|
679e0228a8 | ||
|
|
e153394323 | ||
|
|
5bd1fcfcfd | ||
|
|
2a392ddc44 | ||
|
|
b5cb8bc0d9 | ||
|
|
fa170bcf98 | ||
|
|
7939d46949 | ||
|
|
ab9df8201a | ||
|
|
4a670ec091 | ||
|
|
10e57e59c4 | ||
|
|
b9ec43ef34 | ||
|
|
42197cd375 | ||
|
|
704852973b | ||
|
|
056b4200df | ||
|
|
250a7d8627 | ||
|
|
1ba51e161e | ||
|
|
32e58af896 | ||
|
|
312fa6fe76 | ||
|
|
afbe0837ba | ||
|
|
36ad2a720f | ||
|
|
901e3b14bb | ||
|
|
588d209f7b | ||
|
|
554c54e6be | ||
|
|
b0fac34ffc | ||
|
|
5ede9f7c6b | ||
|
|
c7254fd23e | ||
|
|
55fcea04af | ||
|
|
c212c0a6b2 | ||
|
|
a31fd6709a | ||
|
|
e367fd2b73 | ||
|
|
1ca67d0241 | ||
|
|
8ffa952ff9 | ||
|
|
da246fa30b | ||
|
|
13f306742e | ||
|
|
f3815dc45e | ||
|
|
d086254012 | ||
|
|
bc4d5ba097 | ||
|
|
c556783fe3 | ||
|
|
5fba4c12aa | ||
|
|
7e0dde3ece | ||
|
|
fc03e83531 | ||
|
|
4c441077c7 | ||
|
|
4a5ca81e9a | ||
|
|
75eebe8f8c | ||
|
|
271a8cdac5 | ||
|
|
25103c1188 | ||
|
|
d81058e606 | ||
|
|
693df54b3b | ||
|
|
ae6ed99dc4 | ||
|
|
14bd58e741 | ||
|
|
6d35a7a4ba | ||
|
|
46b0d1ceac | ||
|
|
67a66d2fcd | ||
|
|
43e90b57ea | ||
|
|
c80740e590 | ||
|
|
54ccb9611e | ||
|
|
8fcb897800 | ||
|
|
699eda5d1b | ||
|
|
d7d0a83550 | ||
|
|
e3c331c911 | ||
|
|
eda4dd6aec | ||
|
|
8ad7be474d | ||
|
|
a64435f155 | ||
|
|
fa160124d2 | ||
|
|
5765cb8330 | ||
|
|
f472b227bb | ||
|
|
d2b419c42e | ||
|
|
09d4de660f | ||
|
|
728dcd8523 | ||
|
|
93cf9bf6a9 | ||
|
|
50841f5e24 | ||
|
|
fc6d92d1fc | ||
|
|
7162a029bb | ||
|
|
d797ddd668 | ||
|
|
989e8c3aa6 | ||
|
|
08b79af242 | ||
|
|
0d2f346a30 | ||
|
|
39f1d5f5fd | ||
|
|
05008bb7f8 | ||
|
|
be90d6fc45 | ||
|
|
a1bcdf9924 | ||
|
|
b0e001393c | ||
|
|
2d08941f6a | ||
|
|
d0fef1f312 | ||
|
|
68342cb0d4 | ||
|
|
2b419212a7 | ||
|
|
b2cbc7e34d | ||
|
|
61247e575b | ||
|
|
31e18266d1 | ||
|
|
df8a8de889 | ||
|
|
8a037d6b29 | ||
|
|
47b555b98c | ||
|
|
0c2dae475e | ||
|
|
dc676d04d8 | ||
|
|
15b54bff50 | ||
|
|
47bd4b4c0b | ||
|
|
3c8b36ddfe | ||
|
|
608df3fddd | ||
|
|
c092c285ee | ||
|
|
93b745e379 | ||
|
|
c18db77ade | ||
|
|
2c0b167e6b | ||
|
|
313254d0c8 | ||
|
|
6f519c97d3 | ||
|
|
17a3e16b1d | ||
|
|
8199358088 | ||
|
|
412928eeaa | ||
|
|
51e1b935bd | ||
|
|
742b51e5e2 | ||
|
|
fdb5e2eebb | ||
|
|
0192f64cd2 | ||
|
|
193298ac87 | ||
|
|
a81cb81799 | ||
|
|
ad8a7fdc9b | ||
|
|
5440afcebe | ||
|
|
715d7e664c | ||
|
|
aa182cfa68 | ||
|
|
f92dd7a872 | ||
|
|
b02b9197d0 | ||
|
|
86d02be70c | ||
|
|
cb990978ee | ||
|
|
a103202c92 | ||
|
|
9d7b133037 | ||
|
|
f727f2a1a9 | ||
|
|
03034768d9 | ||
|
|
aed3e20e08 | ||
|
|
74bac6d986 | ||
|
|
7ebecc353a | ||
|
|
f0302b0d1e | ||
|
|
0b004ad089 |
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
|||||||
* @JoeMatt @lonkelle @jkcoxson
|
* @JoeMatt @lonkelle
|
||||||
|
|||||||
40
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report a bug
|
||||||
|
title: "[BUG] "
|
||||||
|
labels: ["bug"]
|
||||||
|
assignees:
|
||||||
|
- naturecodevoid
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report! Before you continue filling out the report, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the bug you are experiencing** in case it has already been reported.
|
||||||
|
|
||||||
|
**Please use [Discord](https://discord.gg/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: What is the bug and how did you discover it?
|
||||||
|
placeholder: Please be clear and concise with your description.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: how-to-reproduce
|
||||||
|
attributes:
|
||||||
|
label: Instructions to reproduce
|
||||||
|
description: Please include clear and consistent instructions for reproducing the bug to make it easier for us to fix it.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: app-version
|
||||||
|
attributes:
|
||||||
|
label: What version of SideStore are you using?
|
||||||
|
description: To retrieve this, go to `Settings` in the SideStore app and scroll down to the bottom.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: other-info
|
||||||
|
attributes:
|
||||||
|
label: Other info
|
||||||
|
description: If you have any other comments, other info that might be useful, or if you found a workaround, please put it here.
|
||||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# force issue template usage
|
||||||
|
blank_issues_enabled: false
|
||||||
|
|
||||||
|
contact_links:
|
||||||
|
- name: Discord
|
||||||
|
url: https://discord.gg/RgpFBX3Q3k
|
||||||
|
about: If you need support, please go here first instead of making an issue!
|
||||||
|
- name: GitHub Discussions
|
||||||
|
url: https://github.com/SideStore/SideStore/discussions
|
||||||
|
about: As an alternative to Discord, you can also make a new GitHub discussion.
|
||||||
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest a feature
|
||||||
|
title: "[FEATURE REQUEST] "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
assignees:
|
||||||
|
- naturecodevoid
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this feature request! Before you continue filling out the form, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the feature you are suggestion** in case it has already been suggested.
|
||||||
|
|
||||||
|
**Please use [Discord](https://discord.gg/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the feature
|
||||||
|
description: What is the feature? How would it work?
|
||||||
|
placeholder: Please be clear and concise with your description.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: use-cases
|
||||||
|
attributes:
|
||||||
|
label: Use cases
|
||||||
|
description: Please include multiple use cases where this feature would be useful.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives
|
||||||
|
description: If you have alternative ideas of how this feature could work, you can put them here.
|
||||||
15
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
### Changes
|
||||||
|
|
||||||
|
<!-- Fill this list with what your PR changes. Example: -->
|
||||||
|
- Fix bug
|
||||||
|
- Change UI for QOL
|
||||||
|
|
||||||
|
<!-- If your PR is ready to be merged, you can remove this section. -->
|
||||||
|
### Todo before merge
|
||||||
|
|
||||||
|
<!-- Example: -->
|
||||||
|
- [x] Finish UI changes
|
||||||
|
- [ ] Test
|
||||||
|
|
||||||
|
<!-- If your PR doesn't close an issue, you can remove the next line. -->
|
||||||
|
Closes #1234
|
||||||
22
.github/workflows/attach_build_products.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Add artifact links to pull request and related issues
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [Pull Request SideStore build]
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
artifacts-url-comments:
|
||||||
|
name: add artifact links to pull request and related issues job
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
steps:
|
||||||
|
- name: add artifact links to pull request and related issues step
|
||||||
|
uses: tonyhallett/artifacts-url-comments@v1.1.0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
prefix: Builds for this Pull Request are available at
|
||||||
|
suffix: Have a nice day.
|
||||||
|
format: name
|
||||||
|
addTo: pull
|
||||||
|
# addTo: pullandissues
|
||||||
96
.github/workflows/beta.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
name: Beta SideStore build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' # example: 1.0.0-beta.1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and upload SideStore Beta
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'macos-12'
|
||||||
|
version: '14.2'
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: brew install ldid
|
||||||
|
|
||||||
|
- name: Change version to tag
|
||||||
|
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: version
|
||||||
|
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Echo version
|
||||||
|
run: echo "${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
- name: Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||||
|
with:
|
||||||
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
|
- name: Build SideStore
|
||||||
|
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
- name: Fakesign app
|
||||||
|
run: make fakesign
|
||||||
|
|
||||||
|
- name: Convert to IPA
|
||||||
|
run: make ipa
|
||||||
|
|
||||||
|
- name: Get current date
|
||||||
|
id: date
|
||||||
|
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Get current date in AltStore date form
|
||||||
|
id: date_altstore
|
||||||
|
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload to new beta release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
name: ${{ steps.version.outputs.version }}
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
draft: true
|
||||||
|
prerelease: true
|
||||||
|
files: SideStore.ipa
|
||||||
|
body: |
|
||||||
|
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||||
|
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal. However, **they might contain bugs and other issues. Use at your own risk!**
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
- TODO
|
||||||
|
|
||||||
|
## Build Info
|
||||||
|
|
||||||
|
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||||
|
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||||
|
Commit SHA: `${{ github.sha }}`
|
||||||
|
Version: `${{ steps.version.outputs.version }}`
|
||||||
|
|
||||||
|
- name: Add version to IPA file name
|
||||||
|
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload SideStore.ipa Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload *.dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
|
path: ./*.dSYM/
|
||||||
100
.github/workflows/build.yml
vendored
@@ -1,100 +0,0 @@
|
|||||||
name: Build and Upload SideStore
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build and upload SideStore
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: 'macos-12'
|
|
||||||
version: '14.0.0'
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Cache rust cargo
|
|
||||||
id: cache-rust-cargo
|
|
||||||
uses: actions/cache@v3
|
|
||||||
env:
|
|
||||||
cache-name: cache-rust-cargo
|
|
||||||
with:
|
|
||||||
path: ~/.cargo
|
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
|
||||||
${{ runner.os }}-build-
|
|
||||||
${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Cache rust minimuxer
|
|
||||||
id: cache-rust-minimuxer
|
|
||||||
uses: actions/cache@v3
|
|
||||||
env:
|
|
||||||
cache-name: cache-rust-minimuxer
|
|
||||||
with:
|
|
||||||
path: ./Dependencies/minimuxer/target
|
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
|
||||||
${{ runner.os }}-build-
|
|
||||||
${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Cache rust em_proxy
|
|
||||||
id: cache-rust-em_proxy
|
|
||||||
uses: actions/cache@v3
|
|
||||||
env:
|
|
||||||
cache-name: cache-rust-em_proxy
|
|
||||||
with:
|
|
||||||
path: ./Dependencies/em_proxy/target
|
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
|
||||||
${{ runner.os }}-build-
|
|
||||||
${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: brew install ldid
|
|
||||||
- name: Install rustup
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
target: aarch64-apple-ios
|
|
||||||
- name: Create emotional damage
|
|
||||||
run: cd Dependencies/em_proxy && cargo build --release --target aarch64-apple-ios
|
|
||||||
- name: Build minimuxer
|
|
||||||
run: cd Dependencies/minimuxer && cargo build --release --target aarch64-apple-ios
|
|
||||||
- name: Setup Xcode
|
|
||||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
|
||||||
with:
|
|
||||||
xcode-version: ${{ matrix.version }}
|
|
||||||
- name: Build SideStore
|
|
||||||
run: |
|
|
||||||
rm -rf ~/Library/Developer/Xcode/DerivedData/
|
|
||||||
rm ./AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
|
|
||||||
xcodebuild -project AltStore.xcodeproj -scheme AltStore -sdk iphoneos archive -archivePath ./archive CODE_SIGNING_REQUIRED=NO AD_HOC_CODE_SIGNING_ALLOWED=YES CODE_SIGNING_ALLOWED=NO DEVELOPMENT_TEAM=XYZ0123456 ORG_IDENTIFIER=com.SideStore | xcpretty && exit ${PIPESTATUS[0]}
|
|
||||||
- name: Fakesign app
|
|
||||||
run: |
|
|
||||||
rm -rf archive.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
|
|
||||||
ldid -SAltStore/Resources/tempEnt.plist archive.xcarchive/Products/Applications/SideStore.app/SideStore
|
|
||||||
- name: Convert to IPA
|
|
||||||
run: |
|
|
||||||
mkdir Payload
|
|
||||||
mkdir Payload/SideStore.app
|
|
||||||
cp -R archive.xcarchive/Products/Applications/SideStore.app/ Payload/SideStore.app/
|
|
||||||
zip -r SideStore.ipa Payload
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v3.1.0
|
|
||||||
with:
|
|
||||||
name: SideStore.ipa
|
|
||||||
path: SideStore.ipa
|
|
||||||
28
.github/workflows/increase-nightly-build-num.sh
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Ensure we are in root directory
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
|
DATE=`date -u +'%Y.%m.%d'`
|
||||||
|
BUILD_NUM=1
|
||||||
|
|
||||||
|
write() {
|
||||||
|
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
|
||||||
|
echo "$DATE,$BUILD_NUM" > .nightly-build-num
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ! -f ".nightly-build-num" ]; then
|
||||||
|
write
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
LAST_DATE=`cat .nightly-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
|
||||||
|
LAST_BUILD_NUM=`cat .nightly-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $2'`
|
||||||
|
|
||||||
|
if [[ "$DATE" != "$LAST_DATE" ]]; then
|
||||||
|
write
|
||||||
|
else
|
||||||
|
BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
|
||||||
|
write
|
||||||
|
fi
|
||||||
|
|
||||||
106
.github/workflows/nightly.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
name: Nightly SideStore build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and upload SideStore Nightly
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'macos-12'
|
||||||
|
version: '14.2'
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: brew install ldid
|
||||||
|
|
||||||
|
- name: Cache .nightly-build-num
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: .nightly-build-num
|
||||||
|
key: nightly-build-num
|
||||||
|
|
||||||
|
- name: Increase nightly build number and set as version
|
||||||
|
run: bash .github/workflows/increase-nightly-build-num.sh
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: version
|
||||||
|
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Echo version
|
||||||
|
run: echo "${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
- name: Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||||
|
with:
|
||||||
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
|
- name: Build SideStore
|
||||||
|
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
- name: Fakesign app
|
||||||
|
run: make fakesign
|
||||||
|
|
||||||
|
- name: Convert to IPA
|
||||||
|
run: make ipa
|
||||||
|
|
||||||
|
- name: Get current date
|
||||||
|
id: date
|
||||||
|
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Get current date in AltStore date form
|
||||||
|
id: date_altstore
|
||||||
|
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload to nightly release
|
||||||
|
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
release: "Nightly"
|
||||||
|
tag: "nightly"
|
||||||
|
prerelease: true
|
||||||
|
files: SideStore.ipa
|
||||||
|
body: |
|
||||||
|
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||||
|
|
||||||
|
Nightly builds are **extremely experimental builds only meant to be used by developers and alpha testers. They often contain bugs and experimental features. Use at your own risk!**
|
||||||
|
|
||||||
|
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore Beta](https://github.com/${{ github.repository }}/releases?q=beta).
|
||||||
|
|
||||||
|
## Build Info
|
||||||
|
|
||||||
|
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||||
|
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||||
|
Commit SHA: `${{ github.sha }}`
|
||||||
|
Version: `${{ steps.version.outputs.version }}`
|
||||||
|
|
||||||
|
- name: Add version to IPA file name
|
||||||
|
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload SideStore.ipa Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload *.dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
|
path: ./*.dSYM/
|
||||||
|
|
||||||
|
- name: Reset cache for apps.sidestore.io/nightly
|
||||||
|
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}
|
||||||
64
.github/workflows/pr.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
name: Pull Request SideStore build
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and upload SideStore
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'macos-12'
|
||||||
|
version: '14.2'
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: brew install ldid
|
||||||
|
|
||||||
|
- name: Add PR suffix to version
|
||||||
|
run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short ${COMMIT:-HEAD})/" -i '' Build.xcconfig
|
||||||
|
env:
|
||||||
|
COMMIT: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: version
|
||||||
|
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Echo version
|
||||||
|
run: echo "${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
- name: Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||||
|
with:
|
||||||
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
|
- name: Build SideStore
|
||||||
|
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
- name: Fakesign app
|
||||||
|
run: make fakesign
|
||||||
|
|
||||||
|
- name: Convert to IPA
|
||||||
|
run: make ipa
|
||||||
|
|
||||||
|
- name: Add version to IPA file name
|
||||||
|
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload SideStore.ipa Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload *.dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
|
path: ./*.dSYM/
|
||||||
93
.github/workflows/stable.yml
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
name: Stable SideStore build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and upload SideStore
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'macos-12'
|
||||||
|
version: '14.2'
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: brew install ldid
|
||||||
|
|
||||||
|
- name: Change version to tag
|
||||||
|
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: version
|
||||||
|
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Echo version
|
||||||
|
run: echo "${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
- name: Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||||
|
with:
|
||||||
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
|
- name: Build SideStore
|
||||||
|
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
- name: Fakesign app
|
||||||
|
run: make fakesign
|
||||||
|
|
||||||
|
- name: Convert to IPA
|
||||||
|
run: make ipa
|
||||||
|
|
||||||
|
- name: Get current date
|
||||||
|
id: date
|
||||||
|
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Get current date in AltStore date form
|
||||||
|
id: date_altstore
|
||||||
|
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload to new stable release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
name: ${{ steps.version.outputs.version }}
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
draft: true
|
||||||
|
files: SideStore.ipa
|
||||||
|
body: |
|
||||||
|
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
- TODO
|
||||||
|
|
||||||
|
## Build Info
|
||||||
|
|
||||||
|
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||||
|
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||||
|
Commit SHA: `${{ github.sha }}`
|
||||||
|
Version: `${{ steps.version.outputs.version }}`
|
||||||
|
|
||||||
|
- name: Add version to IPA file name
|
||||||
|
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload SideStore.ipa Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload *.dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
|
path: ./*.dSYM/
|
||||||
12
.gitignore
vendored
@@ -33,4 +33,14 @@ xcuserdata
|
|||||||
/.vscode
|
/.vscode
|
||||||
|
|
||||||
## AppCode specific
|
## AppCode specific
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
Payload/
|
||||||
|
SideStore.ipa
|
||||||
|
*.dSYM
|
||||||
|
|
||||||
|
Dependencies/.*-prebuilt-fetch-*
|
||||||
|
Dependencies/minimuxer/*
|
||||||
|
Dependencies/em_proxy/*
|
||||||
|
!Dependencies/**/.gitkeep
|
||||||
|
.nightly-build-num
|
||||||
|
|||||||
9
.gitmodules
vendored
@@ -13,12 +13,9 @@
|
|||||||
[submodule "Dependencies/MarkdownAttributedString"]
|
[submodule "Dependencies/MarkdownAttributedString"]
|
||||||
path = Dependencies/MarkdownAttributedString
|
path = Dependencies/MarkdownAttributedString
|
||||||
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
||||||
[submodule "Dependencies/em_proxy"]
|
|
||||||
path = Dependencies/em_proxy
|
|
||||||
url = https://github.com/jkcoxson/em_proxy
|
|
||||||
[submodule "Dependencies/libimobiledevice-glue"]
|
[submodule "Dependencies/libimobiledevice-glue"]
|
||||||
path = Dependencies/libimobiledevice-glue
|
path = Dependencies/libimobiledevice-glue
|
||||||
url = https://github.com/libimobiledevice/libimobiledevice-glue
|
url = https://github.com/libimobiledevice/libimobiledevice-glue
|
||||||
[submodule "Dependencies/minimuxer"]
|
[submodule "Dependencies/libfragmentzip"]
|
||||||
path = Dependencies/minimuxer
|
path = Dependencies/libfragmentzip
|
||||||
url = https://github.com/jkcoxson/minimuxer
|
url = https://github.com/SideStore/libfragmentzip.git
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<key>ALTAppGroups</key>
|
<key>ALTAppGroups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||||
<string>group.com.rileytestut.AltStore</string>
|
<string>group.com.SideStore.SideStore</string>
|
||||||
</array>
|
</array>
|
||||||
<key>ALTBundleIdentifier</key>
|
<key>ALTBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ extension XPCConnectionHandler: NSXPCListenerDelegate
|
|||||||
guard
|
guard
|
||||||
let codeSigningInfo = signingInfo as? [String: Any],
|
let codeSigningInfo = signingInfo as? [String: Any],
|
||||||
let bundleIdentifier = codeSigningInfo["identifier"] as? String,
|
let bundleIdentifier = codeSigningInfo["identifier"] as? String,
|
||||||
bundleIdentifier.contains("com.rileytestut.AltStore")
|
bundleIdentifier.contains(Bundle.Info.appbundleIdentifier)
|
||||||
else { return false }
|
else { return false }
|
||||||
|
|
||||||
let connection = XPCConnection(newConnection)
|
let connection = XPCConnection(newConnection)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#include "Build.xcconfig"
|
#include "Build.xcconfig"
|
||||||
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).$(PRODUCT_NAME)
|
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER)
|
||||||
|
|||||||
@@ -18,6 +18,24 @@
|
|||||||
"version" : "4.4.2"
|
"version" : "4.4.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "asyncimage",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/fabianthdev/AsyncImage",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "main",
|
||||||
|
"revision" : "018a4fffea025066d795ebb025c2769183f3fffb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "expandabletext",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/fabianthdev/ExpandableText",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "main",
|
||||||
|
"revision" : "a375f5b8c73f0af69aa7add890378fdf404a29bc"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "keychainaccess",
|
"identity" : "keychainaccess",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -36,6 +54,15 @@
|
|||||||
"version" : "4.2.0"
|
"version" : "4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "localconsole",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/duraidabdul/LocalConsole.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2c5d5e018acd4963fe6dfe858f6d6fecef7cbf2f",
|
||||||
|
"version" : "1.12.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "nuke",
|
"identity" : "nuke",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -63,6 +90,33 @@
|
|||||||
"version" : "1.10.1"
|
"version" : "1.10.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "reachability.swift",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/ashleymills/Reachability.swift",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "a81b7367f2c46875f29577e03a60c39cdfad0c8d"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "semanticversion",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "fc670910dc0903cc269b3d0b776cda5703979c4e",
|
||||||
|
"version" : "0.3.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sfsafesymbols",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "50bc33264e6c0972f905b61af656201cf6091de8",
|
||||||
|
"version" : "4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "sparkle",
|
"identity" : "sparkle",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -72,6 +126,15 @@
|
|||||||
"version" : "2.1.0"
|
"version" : "2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "starscream",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/daltoniam/Starscream.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21",
|
||||||
|
"version" : "4.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "stprivilegedtask",
|
"identity" : "stprivilegedtask",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1020"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||||
|
BuildableName = "SideStore.app"
|
||||||
|
BlueprintName = "SideStore"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||||
|
BuildableName = "SideStore.app"
|
||||||
|
BlueprintName = "SideStore"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<CommandLineArguments>
|
||||||
|
<CommandLineArgument
|
||||||
|
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</CommandLineArgument>
|
||||||
|
</CommandLineArguments>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||||
|
BuildableName = "SideStore.app"
|
||||||
|
BlueprintName = "SideStore"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Release">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -14,13 +14,7 @@ import AppCenter
|
|||||||
import AppCenterAnalytics
|
import AppCenterAnalytics
|
||||||
import AppCenterCrashes
|
import AppCenterCrashes
|
||||||
|
|
||||||
#if DEBUG
|
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
|
||||||
private let appCenterAppSecret = "bb08e9bb-c126-408d-bf3f-324c8473fd40"
|
|
||||||
#elseif RELEASE
|
|
||||||
private let appCenterAppSecret = "b6718932-294a-432b-81f2-be1e17ff85c5"
|
|
||||||
#else
|
|
||||||
private let appCenterAppSecret = "e873f6ca-75eb-4685-818f-801e0e375d60"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
extension AnalyticsManager
|
extension AnalyticsManager
|
||||||
{
|
{
|
||||||
@@ -77,7 +71,7 @@ extension AnalyticsManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AnalyticsManager
|
final class AnalyticsManager
|
||||||
{
|
{
|
||||||
static let shared = AnalyticsManager()
|
static let shared = AnalyticsManager()
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ extension AppContentViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppContentViewController: UITableViewController
|
final class AppContentViewController: UITableViewController
|
||||||
{
|
{
|
||||||
var app: StoreApp!
|
var app: StoreApp!
|
||||||
|
|
||||||
@@ -80,10 +80,21 @@ class AppContentViewController: UITableViewController
|
|||||||
|
|
||||||
self.subtitleLabel.text = self.app.subtitle
|
self.subtitleLabel.text = self.app.subtitle
|
||||||
self.descriptionTextView.text = self.app.localizedDescription
|
self.descriptionTextView.text = self.app.localizedDescription
|
||||||
self.versionDescriptionTextView.text = self.app.versionDescription
|
|
||||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), self.app.version)
|
if let version = self.app.latestVersion
|
||||||
self.versionDateLabel.text = Date().relativeDateString(since: self.app.versionDate, dateFormatter: self.dateFormatter)
|
{
|
||||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: Int64(self.app.size))
|
self.versionDescriptionTextView.text = version.localizedDescription
|
||||||
|
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
|
||||||
|
self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
|
||||||
|
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.versionDescriptionTextView.text = nil
|
||||||
|
self.versionLabel.text = nil
|
||||||
|
self.versionDateLabel.text = nil
|
||||||
|
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
|
||||||
|
}
|
||||||
|
|
||||||
self.descriptionTextView.maximumNumberOfLines = 5
|
self.descriptionTextView.maximumNumberOfLines = 5
|
||||||
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||||
@@ -173,7 +184,8 @@ private extension AppContentViewController
|
|||||||
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
|
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
|
||||||
let cell = cell as! PermissionCollectionViewCell
|
let cell = cell as! PermissionCollectionViewCell
|
||||||
cell.button.setImage(permission.type.icon, for: .normal)
|
cell.button.setImage(permission.type.icon, for: .normal)
|
||||||
cell.textLabel.text = permission.type.localizedShortName
|
cell.button.tintColor = .label
|
||||||
|
cell.textLabel.text = permission.type.localizedShortName ?? permission.type.localizedName
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataSource
|
return dataSource
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class PermissionCollectionViewCell: UICollectionViewCell
|
final class PermissionCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
@IBOutlet var button: UIButton!
|
@IBOutlet var button: UIButton!
|
||||||
@IBOutlet var textLabel: UILabel!
|
@IBOutlet var textLabel: UILabel!
|
||||||
@@ -29,7 +29,7 @@ class PermissionCollectionViewCell: UICollectionViewCell
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppContentTableViewCell: UITableViewCell
|
final class AppContentTableViewCell: UITableViewCell
|
||||||
{
|
{
|
||||||
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import Roxas
|
|||||||
|
|
||||||
import Nuke
|
import Nuke
|
||||||
|
|
||||||
class AppViewController: UIViewController
|
final class AppViewController: UIViewController
|
||||||
{
|
{
|
||||||
var app: StoreApp!
|
var app: StoreApp!
|
||||||
|
|
||||||
@@ -352,7 +352,7 @@ class AppViewController: UIViewController
|
|||||||
|
|
||||||
extension AppViewController
|
extension AppViewController
|
||||||
{
|
{
|
||||||
class func makeAppViewController(app: StoreApp) -> AppViewController
|
final class func makeAppViewController(app: StoreApp) -> AppViewController
|
||||||
{
|
{
|
||||||
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
||||||
|
|
||||||
@@ -384,10 +384,10 @@ private extension AppViewController
|
|||||||
button.progress = progress
|
button.progress = progress
|
||||||
}
|
}
|
||||||
|
|
||||||
if Date() < self.app.versionDate
|
if let versionDate = self.app.latestVersion?.date, versionDate > Date()
|
||||||
{
|
{
|
||||||
self.bannerView.button.countdownDate = self.app.versionDate
|
self.bannerView.button.countdownDate = versionDate
|
||||||
self.navigationBarDownloadButton.countdownDate = self.app.versionDate
|
self.navigationBarDownloadButton.countdownDate = versionDate
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import UIKit
|
|||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
|
|
||||||
class PermissionPopoverViewController: UIViewController
|
final class PermissionPopoverViewController: UIViewController
|
||||||
{
|
{
|
||||||
var permission: AppPermission!
|
var permission: AppPermission!
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import UIKit
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
class AppIDsViewController: UICollectionViewController
|
final class AppIDsViewController: UICollectionViewController
|
||||||
{
|
{
|
||||||
private lazy var dataSource = self.makeDataSource()
|
private lazy var dataSource = self.makeDataSource()
|
||||||
|
|
||||||
|
|||||||
18
AltStore/App/SideStoreUIApp.swift
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// SideStoreUIApp.swift
|
||||||
|
// SideStoreUI
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 18.11.22.
|
||||||
|
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct SideStoreUIApp: App {
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
RootView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,49 +18,52 @@ import EmotionalDamage
|
|||||||
|
|
||||||
extension AppDelegate
|
extension AppDelegate
|
||||||
{
|
{
|
||||||
static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
|
static let openPatreonSettingsDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".OpenPatreonSettingsDeepLinkNotification")
|
||||||
static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification")
|
static let importAppDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ImportAppDeepLinkNotification")
|
||||||
static let addSourceDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.AddSourceDeepLinkNotification")
|
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
|
||||||
|
|
||||||
static let appBackupDidFinish = Notification.Name("com.rileytestut.AltStore.AppBackupDidFinish")
|
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
|
||||||
|
|
||||||
static let importAppDeepLinkURLKey = "fileURL"
|
static let importAppDeepLinkURLKey = "fileURL"
|
||||||
static let appBackupResultKey = "result"
|
static let appBackupResultKey = "result"
|
||||||
static let addSourceDeepLinkURLKey = "sourceURL"
|
static let addSourceDeepLinkURLKey = "sourceURL"
|
||||||
}
|
}
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
private var intentHandler: IntentHandler {
|
private var intentHandler: IntentHandler {
|
||||||
get { _intentHandler as! IntentHandler }
|
get { _intentHandler as! IntentHandler }
|
||||||
set { _intentHandler = newValue }
|
set { _intentHandler = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
private var viewAppIntentHandler: ViewAppIntentHandler {
|
private var viewAppIntentHandler: ViewAppIntentHandler {
|
||||||
get { _viewAppIntentHandler as! ViewAppIntentHandler }
|
get { _viewAppIntentHandler as! ViewAppIntentHandler }
|
||||||
set { _viewAppIntentHandler = newValue }
|
set { _viewAppIntentHandler = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var _intentHandler: Any = {
|
private lazy var _intentHandler: Any = {
|
||||||
guard #available(iOS 14, *) else { fatalError() }
|
guard #available(iOS 14, *) else { fatalError() }
|
||||||
return IntentHandler()
|
return IntentHandler()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private lazy var _viewAppIntentHandler: Any = {
|
private lazy var _viewAppIntentHandler: Any = {
|
||||||
guard #available(iOS 14, *) else { fatalError() }
|
guard #available(iOS 14, *) else { fatalError() }
|
||||||
return ViewAppIntentHandler()
|
return ViewAppIntentHandler()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||||
{
|
{
|
||||||
|
// Copy STDOUT and STDERR to the logging console
|
||||||
|
_ = OutputCapturer.shared
|
||||||
|
|
||||||
// Register default settings before doing anything else.
|
// Register default settings before doing anything else.
|
||||||
UserDefaults.registerDefaults()
|
UserDefaults.registerDefaults()
|
||||||
|
|
||||||
DatabaseManager.shared.start { (error) in
|
DatabaseManager.shared.start { (error) in
|
||||||
if let error = error
|
if let error = error
|
||||||
{
|
{
|
||||||
@@ -71,50 +74,62 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
print("Started DatabaseManager.")
|
print("Started DatabaseManager.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnalyticsManager.shared.start()
|
AnalyticsManager.shared.start()
|
||||||
|
|
||||||
self.setTintColor()
|
self.setTintColor()
|
||||||
|
|
||||||
SecureValueTransformer.register()
|
SecureValueTransformer.register()
|
||||||
|
|
||||||
if UserDefaults.standard.firstLaunch == nil
|
if UserDefaults.standard.firstLaunch == nil
|
||||||
{
|
{
|
||||||
Keychain.shared.reset()
|
Keychain.shared.reset()
|
||||||
UserDefaults.standard.firstLaunch = Date()
|
UserDefaults.standard.firstLaunch = Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
|
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
|
||||||
|
|
||||||
#if DEBUG || BETA
|
#if DEBUG || BETA
|
||||||
UserDefaults.standard.isDebugModeEnabled = true
|
UserDefaults.standard.isDebugModeEnabled = true
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
self.prepareForBackgroundFetch()
|
self.prepareForBackgroundFetch()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidEnterBackground(_ application: UIApplication)
|
func applicationDidEnterBackground(_ application: UIApplication)
|
||||||
{
|
{
|
||||||
|
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
|
||||||
|
|
||||||
|
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
|
||||||
|
|
||||||
|
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
|
||||||
|
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .success: break
|
||||||
|
case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillEnterForeground(_ application: UIApplication)
|
func applicationWillEnterForeground(_ application: UIApplication)
|
||||||
{
|
{
|
||||||
AppManager.shared.update()
|
AppManager.shared.update()
|
||||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
||||||
{
|
{
|
||||||
return self.open(url)
|
return self.open(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
|
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
|
||||||
{
|
{
|
||||||
guard #available(iOS 14, *) else { return nil }
|
guard #available(iOS 14, *) else { return nil }
|
||||||
|
|
||||||
switch intent
|
switch intent
|
||||||
{
|
{
|
||||||
case is RefreshAllIntent: return self.intentHandler
|
case is RefreshAllIntent: return self.intentHandler
|
||||||
@@ -133,7 +148,7 @@ extension AppDelegate
|
|||||||
// Use this method to select a configuration to create the new scene with.
|
// Use this method to select a configuration to create the new scene with.
|
||||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
|
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
|
||||||
{
|
{
|
||||||
// Called when the user discards a scene session.
|
// Called when the user discards a scene session.
|
||||||
@@ -148,36 +163,36 @@ private extension AppDelegate
|
|||||||
{
|
{
|
||||||
self.window?.tintColor = .altPrimary
|
self.window?.tintColor = .altPrimary
|
||||||
}
|
}
|
||||||
|
|
||||||
func open(_ url: URL) -> Bool
|
func open(_ url: URL) -> Bool
|
||||||
{
|
{
|
||||||
if url.isFileURL
|
if url.isFileURL
|
||||||
{
|
{
|
||||||
guard url.pathExtension.lowercased() == "ipa" else { return false }
|
guard url.pathExtension.lowercased() == "ipa" else { return false }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url])
|
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url])
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
||||||
guard let host = components.host?.lowercased() else { return false }
|
guard let host = components.host?.lowercased() else { return false }
|
||||||
|
|
||||||
switch host
|
switch host
|
||||||
{
|
{
|
||||||
case "patreon":
|
case "patreon":
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case "appbackupresponse":
|
case "appbackupresponse":
|
||||||
let result: Result<Void, Error>
|
let result: Result<Void, Error>
|
||||||
|
|
||||||
switch url.path.lowercased()
|
switch url.path.lowercased()
|
||||||
{
|
{
|
||||||
case "/success": result = .success(())
|
case "/success": result = .success(())
|
||||||
@@ -188,37 +203,37 @@ private extension AppDelegate
|
|||||||
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
|
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
|
||||||
let errorDescription = queryItems["errorDescription"]
|
let errorDescription = queryItems["errorDescription"]
|
||||||
else { return false }
|
else { return false }
|
||||||
|
|
||||||
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
|
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
|
||||||
result = .failure(error)
|
result = .failure(error)
|
||||||
|
|
||||||
default: return false
|
default: return false
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
|
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case "install":
|
case "install":
|
||||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
||||||
guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false }
|
guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
|
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case "source":
|
case "source":
|
||||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
||||||
guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false }
|
guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
|
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
default: return false
|
default: return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,47 +246,47 @@ extension AppDelegate
|
|||||||
{
|
{
|
||||||
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
|
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
|
||||||
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
|
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
|
||||||
|
|
||||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
UIApplication.shared.registerForRemoteNotifications()
|
UIApplication.shared.registerForRemoteNotifications()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||||
{
|
{
|
||||||
let tokenParts = deviceToken.map { data -> String in
|
let tokenParts = deviceToken.map { data -> String in
|
||||||
return String(format: "%02.2hhx", data)
|
return String(format: "%02.2hhx", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = tokenParts.joined()
|
let token = tokenParts.joined()
|
||||||
print("Push Token:", token)
|
print("Push Token:", token)
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
||||||
{
|
{
|
||||||
self.application(application, performFetchWithCompletionHandler: completionHandler)
|
self.application(application, performFetchWithCompletionHandler: completionHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
||||||
{
|
{
|
||||||
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
|
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
|
||||||
{
|
{
|
||||||
let threeHours: TimeInterval = 3 * 60 * 60
|
let threeHours: TimeInterval = 3 * 60 * 60
|
||||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
|
||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = NSLocalizedString("App Refresh Tip", comment: "")
|
content.title = NSLocalizedString("App Refresh Tip", comment: "")
|
||||||
content.body = NSLocalizedString("The more you open SideStore, the more chances it's given to refresh apps in the background.", comment: "")
|
content.body = NSLocalizedString("The more you open SideStore, the more chances it's given to refresh apps in the background.", comment: "")
|
||||||
|
|
||||||
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
|
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
|
||||||
UNUserNotificationCenter.current().add(request)
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
|
||||||
UserDefaults.standard.presentedLaunchReminderNotification = true
|
UserDefaults.standard.presentedLaunchReminderNotification = true
|
||||||
}
|
}
|
||||||
|
|
||||||
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
|
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
|
||||||
if let error = taskResult.error
|
if let error = taskResult.error
|
||||||
{
|
{
|
||||||
@@ -280,7 +295,7 @@ extension AppDelegate
|
|||||||
taskCompletionHandler()
|
taskCompletionHandler()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !DatabaseManager.shared.isStarted
|
if !DatabaseManager.shared.isStarted
|
||||||
{
|
{
|
||||||
DatabaseManager.shared.start() { (error) in
|
DatabaseManager.shared.start() { (error) in
|
||||||
@@ -309,7 +324,7 @@ extension AppDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
||||||
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
|
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
|
||||||
{
|
{
|
||||||
@@ -319,15 +334,15 @@ extension AppDelegate
|
|||||||
case .failure: backgroundFetchCompletionHandler(.failed)
|
case .failure: backgroundFetchCompletionHandler(.failed)
|
||||||
case .success: backgroundFetchCompletionHandler(.newData)
|
case .success: backgroundFetchCompletionHandler(.newData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !UserDefaults.standard.isBackgroundRefreshEnabled
|
if !UserDefaults.standard.isBackgroundRefreshEnabled
|
||||||
{
|
{
|
||||||
refreshAppsCompletionHandler(.success([:]))
|
refreshAppsCompletionHandler(.success([:]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
|
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
|
||||||
|
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
|
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
|
||||||
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
|
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
|
||||||
@@ -343,49 +358,49 @@ private extension AppDelegate
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
let (sources, context) = try result.get()
|
let (sources, context) = try result.get()
|
||||||
|
|
||||||
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
||||||
previousUpdatesFetchRequest.includesPendingChanges = false
|
previousUpdatesFetchRequest.includesPendingChanges = false
|
||||||
previousUpdatesFetchRequest.resultType = .dictionaryResultType
|
previousUpdatesFetchRequest.resultType = .dictionaryResultType
|
||||||
previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)]
|
previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)]
|
||||||
|
|
||||||
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
|
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
|
||||||
previousNewsItemsFetchRequest.includesPendingChanges = false
|
previousNewsItemsFetchRequest.includesPendingChanges = false
|
||||||
previousNewsItemsFetchRequest.resultType = .dictionaryResultType
|
previousNewsItemsFetchRequest.resultType = .dictionaryResultType
|
||||||
previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)]
|
previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)]
|
||||||
|
|
||||||
let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]]
|
let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]]
|
||||||
let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]]
|
let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]]
|
||||||
|
|
||||||
try context.save()
|
try context.save()
|
||||||
|
|
||||||
let updatesFetchRequest = InstalledApp.updatesFetchRequest()
|
let updatesFetchRequest = InstalledApp.updatesFetchRequest()
|
||||||
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
||||||
|
|
||||||
let updates = try context.fetch(updatesFetchRequest)
|
let updates = try context.fetch(updatesFetchRequest)
|
||||||
let newsItems = try context.fetch(newsItemsFetchRequest)
|
let newsItems = try context.fetch(newsItemsFetchRequest)
|
||||||
|
|
||||||
for update in updates
|
for update in updates
|
||||||
{
|
{
|
||||||
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
|
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
|
||||||
guard let storeApp = update.storeApp else { continue }
|
guard let storeApp = update.storeApp, let version = storeApp.version else { continue }
|
||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = NSLocalizedString("New Update Available", comment: "")
|
content.title = NSLocalizedString("New Update Available", comment: "")
|
||||||
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, storeApp.version)
|
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, version)
|
||||||
content.sound = .default
|
content.sound = .default
|
||||||
|
|
||||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||||
UNUserNotificationCenter.current().add(request)
|
UNUserNotificationCenter.current().add(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
for newsItem in newsItems
|
for newsItem in newsItems
|
||||||
{
|
{
|
||||||
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
|
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
|
||||||
guard !newsItem.isSilent else { continue }
|
guard !newsItem.isSilent else { continue }
|
||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
if let app = newsItem.storeApp
|
if let app = newsItem.storeApp
|
||||||
{
|
{
|
||||||
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
|
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
|
||||||
@@ -394,10 +409,10 @@ private extension AppDelegate
|
|||||||
{
|
{
|
||||||
content.title = NSLocalizedString("SideStore News", comment: "")
|
content.title = NSLocalizedString("SideStore News", comment: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
content.body = newsItem.title
|
content.body = newsItem.title
|
||||||
content.sound = .default
|
content.sound = .default
|
||||||
|
|
||||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||||
UNUserNotificationCenter.current().add(request)
|
UNUserNotificationCenter.current().add(request)
|
||||||
}
|
}
|
||||||
@@ -405,7 +420,7 @@ private extension AppDelegate
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
UIApplication.shared.applicationIconBadgeNumber = updates.count
|
UIApplication.shared.applicationIconBadgeNumber = updates.count
|
||||||
}
|
}
|
||||||
|
|
||||||
completionHandler(.success(sources))
|
completionHandler(.success(sources))
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import UIKit
|
|||||||
|
|
||||||
import AltSign
|
import AltSign
|
||||||
|
|
||||||
class AuthenticationViewController: UIViewController
|
final class AuthenticationViewController: UIViewController
|
||||||
{
|
{
|
||||||
var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)?
|
var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)?
|
||||||
var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)?
|
var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)?
|
||||||
@@ -31,7 +31,7 @@ class AuthenticationViewController: UIViewController
|
|||||||
{
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
self.signInButton.activityIndicatorView.style = .white
|
self.signInButton.activityIndicatorView.style = .medium
|
||||||
|
|
||||||
for view in [self.appleIDBackgroundView!, self.passwordBackgroundView!, self.signInButton!]
|
for view in [self.appleIDBackgroundView!, self.passwordBackgroundView!, self.signInButton!]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class InstructionsViewController: UIViewController
|
final class InstructionsViewController: UIViewController
|
||||||
{
|
{
|
||||||
var completionHandler: (() -> Void)?
|
var completionHandler: (() -> Void)?
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import AltStoreCore
|
|||||||
import AltSign
|
import AltSign
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
class RefreshAltStoreViewController: UIViewController
|
final class RefreshAltStoreViewController: UIViewController
|
||||||
{
|
{
|
||||||
var context: AuthenticatedOperationContext!
|
var context: AuthenticatedOperationContext!
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import IntentsUI
|
|||||||
|
|
||||||
import AltSign
|
import AltSign
|
||||||
|
|
||||||
class SelectTeamViewController: UITableViewController
|
final class SelectTeamViewController: UITableViewController
|
||||||
{
|
{
|
||||||
public var teams: [ALTTeam]?
|
public var teams: [ALTTeam]?
|
||||||
public var completionHandler: ((Result<ALTTeam, Swift.Error>) -> Void)?
|
public var completionHandler: ((Result<ALTTeam, Swift.Error>) -> Void)?
|
||||||
|
|||||||
@@ -1095,13 +1095,13 @@ World</string>
|
|||||||
<image name="News" width="19" height="20"/>
|
<image name="News" width="19" height="20"/>
|
||||||
<image name="Settings" width="20" height="20"/>
|
<image name="Settings" width="20" height="20"/>
|
||||||
<namedColor name="Background">
|
<namedColor name="Background">
|
||||||
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
<namedColor name="BlurTint">
|
<namedColor name="BlurTint">
|
||||||
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="1" green="1" blue="1" alpha="0.3" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
<namedColor name="Primary">
|
<namedColor name="Primary">
|
||||||
<color red="0.50196078431372548" green="0.2627450980392157" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
<systemColor name="systemBackgroundColor">
|
<systemColor name="systemBackgroundColor">
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Roxas
|
|||||||
|
|
||||||
import Nuke
|
import Nuke
|
||||||
|
|
||||||
@objc class BrowseCollectionViewCell: UICollectionViewCell
|
@objc final class BrowseCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
var imageURLs: [URL] = [] {
|
var imageURLs: [URL] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ private extension BrowseViewController
|
|||||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||||
|
|
||||||
cell.bannerView.button.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
cell.bannerView.button.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||||
cell.bannerView.button.activityIndicatorView.style = .white
|
cell.bannerView.button.activityIndicatorView.style = .medium
|
||||||
|
|
||||||
// Explicitly set to false to ensure we're starting from a non-activity indicating state.
|
// Explicitly set to false to ensure we're starting from a non-activity indicating state.
|
||||||
// Otherwise, cell reuse can mess up some cached values.
|
// Otherwise, cell reuse can mess up some cached values.
|
||||||
@@ -113,7 +113,7 @@ private extension BrowseViewController
|
|||||||
let progress = AppManager.shared.installationProgress(for: app)
|
let progress = AppManager.shared.installationProgress(for: app)
|
||||||
cell.bannerView.button.progress = progress
|
cell.bannerView.button.progress = progress
|
||||||
|
|
||||||
if Date() < app.versionDate
|
if let versionDate = app.latestVersion?.date, versionDate > Date()
|
||||||
{
|
{
|
||||||
cell.bannerView.button.countdownDate = app.versionDate
|
cell.bannerView.button.countdownDate = app.versionDate
|
||||||
}
|
}
|
||||||
@@ -167,14 +167,7 @@ private extension BrowseViewController
|
|||||||
|
|
||||||
func updateDataSource()
|
func updateDataSource()
|
||||||
{
|
{
|
||||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
|
||||||
{
|
|
||||||
self.dataSource.predicate = nil
|
self.dataSource.predicate = nil
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.dataSource.predicate = NSPredicate(format: "%K == NO", #keyPath(StoreApp.isBeta))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchSource()
|
func fetchSource()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class AppIconImageView: UIImageView
|
final class AppIconImageView: UIImageView
|
||||||
{
|
{
|
||||||
override func awakeFromNib()
|
override func awakeFromNib()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
class BackgroundTaskManager
|
final class BackgroundTaskManager
|
||||||
{
|
{
|
||||||
static let shared = BackgroundTaskManager()
|
static let shared = BackgroundTaskManager()
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class BannerCollectionViewCell: UICollectionViewCell
|
final class BannerCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
private(set) var errorBadge: UIView?
|
private(set) var errorBadge: UIView?
|
||||||
@IBOutlet private(set) var bannerView: AppBannerView!
|
@IBOutlet private(set) var bannerView: AppBannerView!
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class Button: UIButton
|
final class Button: UIButton
|
||||||
{
|
{
|
||||||
override var intrinsicContentSize: CGSize {
|
override var intrinsicContentSize: CGSize {
|
||||||
var size = super.intrinsicContentSize
|
var size = super.intrinsicContentSize
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class CollapsingTextView: UITextView
|
final class CollapsingTextView: UITextView
|
||||||
{
|
{
|
||||||
var isCollapsed = true {
|
var isCollapsed = true {
|
||||||
didSet {
|
didSet {
|
||||||
@@ -71,8 +71,10 @@ class CollapsingTextView: UITextView
|
|||||||
{
|
{
|
||||||
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
|
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
|
||||||
|
|
||||||
let maximumCollapsedHeight = font.lineHeight * CGFloat(self.maximumNumberOfLines)
|
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
|
||||||
if self.intrinsicContentSize.height > maximumCollapsedHeight
|
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines)
|
||||||
|
|
||||||
|
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
|
||||||
{
|
{
|
||||||
var exclusionFrame = moreButtonFrame
|
var exclusionFrame = moreButtonFrame
|
||||||
exclusionFrame.origin.y += self.moreButton.bounds.midY
|
exclusionFrame.origin.y += self.moreButton.bounds.midY
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class ForwardingNavigationController: UINavigationController
|
final class ForwardingNavigationController: UINavigationController
|
||||||
{
|
{
|
||||||
override var childForStatusBarStyle: UIViewController? {
|
override var childForStatusBarStyle: UIViewController? {
|
||||||
return self.topViewController
|
return self.topViewController
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import UIKit
|
|||||||
|
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
class NavigationBar: UINavigationBar
|
final class NavigationBar: UINavigationBar
|
||||||
{
|
{
|
||||||
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true
|
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class PillButton: UIButton
|
final class PillButton: UIButton
|
||||||
{
|
{
|
||||||
override var accessibilityValue: String? {
|
override var accessibilityValue: String? {
|
||||||
get {
|
get {
|
||||||
@@ -88,7 +88,7 @@ class PillButton: UIButton
|
|||||||
self.layer.masksToBounds = true
|
self.layer.masksToBounds = true
|
||||||
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
|
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
|
||||||
|
|
||||||
self.activityIndicatorView.style = .white
|
self.activityIndicatorView.style = .medium
|
||||||
self.activityIndicatorView.isUserInteractionEnabled = false
|
self.activityIndicatorView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.progressView.progress = 0
|
self.progressView.progress = 0
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ extension TimeInterval
|
|||||||
static let longToastViewDuration = 8.0
|
static let longToastViewDuration = 8.0
|
||||||
}
|
}
|
||||||
|
|
||||||
class ToastView: RSTToastView
|
final class ToastView: RSTToastView
|
||||||
{
|
{
|
||||||
var preferredDuration: TimeInterval
|
var preferredDuration: TimeInterval
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
let customLog = OSLog(subsystem: "org.sidestore.sidestore",
|
public let customLog = OSLog(subsystem: "org.sidestore.sidestore",
|
||||||
category: "ios")
|
category: "ios")
|
||||||
|
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ public extension OSLog {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - message: String or format string
|
/// - message: String or format string
|
||||||
/// - args: optional args for format string
|
/// - args: optional args for format string
|
||||||
|
@inlinable
|
||||||
static func error(_ message: StaticString, _ args: CVarArg...) {
|
static func error(_ message: StaticString, _ args: CVarArg...) {
|
||||||
os_log(message, log: customLog, type: .error, args)
|
os_log(message, log: customLog, type: .error, args)
|
||||||
}
|
}
|
||||||
@@ -26,6 +27,7 @@ public extension OSLog {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - message: String or format string
|
/// - message: String or format string
|
||||||
/// - args: optional args for format string
|
/// - args: optional args for format string
|
||||||
|
@inlinable
|
||||||
static func info(_ message: StaticString, _ args: CVarArg...) {
|
static func info(_ message: StaticString, _ args: CVarArg...) {
|
||||||
os_log(message, log: customLog, type: .info, args)
|
os_log(message, log: customLog, type: .info, args)
|
||||||
}
|
}
|
||||||
@@ -34,6 +36,7 @@ public extension OSLog {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - message: String or format string
|
/// - message: String or format string
|
||||||
/// - args: optional args for format string
|
/// - args: optional args for format string
|
||||||
|
@inlinable
|
||||||
static func debug(_ message: StaticString, _ args: CVarArg...) {
|
static func debug(_ message: StaticString, _ args: CVarArg...) {
|
||||||
os_log(message, log: customLog, type: .debug, args)
|
os_log(message, log: customLog, type: .debug, args)
|
||||||
}
|
}
|
||||||
@@ -45,6 +48,7 @@ public extension OSLog {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - message: String or format string
|
/// - message: String or format string
|
||||||
/// - args: optional args for format string
|
/// - args: optional args for format string
|
||||||
|
@inlinable
|
||||||
public func ELOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
|
public func ELOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
|
||||||
OSLog.error(message, args)
|
OSLog.error(message, args)
|
||||||
}
|
}
|
||||||
@@ -53,6 +57,7 @@ public func ELOG(_ message: StaticString, file: StaticString = #file, function:
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - message: String or format string
|
/// - message: String or format string
|
||||||
/// - args: optional args for format string
|
/// - args: optional args for format string
|
||||||
|
@inlinable
|
||||||
public func ILOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
|
public func ILOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
|
||||||
OSLog.info(message, args)
|
OSLog.info(message, args)
|
||||||
}
|
}
|
||||||
|
|||||||
19
AltStore/Extensions/Source+Trusted.swift
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// Source+Trusted.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 04.02.23.
|
||||||
|
// Copyright © 2023 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
extension Source {
|
||||||
|
var isOfficial: Bool {
|
||||||
|
self.identifier == Source.altStoreIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
var isTrusted: Bool {
|
||||||
|
UserDefaults.shared.trustedSourceIDs?.contains(self.identifier) ?? false
|
||||||
|
}
|
||||||
|
}
|
||||||
17
AltStore/Extensions/StoreApp+Filterable.swift
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// StoreApp+Searchable.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 01.12.22.
|
||||||
|
// Copyright © 2022 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
extension StoreApp: Filterable {
|
||||||
|
func matches(_ searchText: String) -> Bool {
|
||||||
|
searchText.isEmpty ||
|
||||||
|
self.name.lowercased().contains(searchText.lowercased()) ||
|
||||||
|
self.developerName.lowercased().contains(searchText.lowercased())
|
||||||
|
}
|
||||||
|
}
|
||||||
19
AltStore/Extensions/StoreApp+Trusted.swift
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// StoreApp+Trusted.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 04.02.23.
|
||||||
|
// Copyright © 2023 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
extension StoreApp {
|
||||||
|
var isFromOfficialSource: Bool {
|
||||||
|
self.source?.isOfficial ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
var isFromTrustedSource: Bool {
|
||||||
|
self.source?.isTrusted ?? false
|
||||||
|
}
|
||||||
|
}
|
||||||
201
AltStore/Generated/Assets.swift
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
// swiftlint:disable all
|
||||||
|
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
import AppKit
|
||||||
|
#elseif os(iOS)
|
||||||
|
import UIKit
|
||||||
|
#elseif os(tvOS) || os(watchOS)
|
||||||
|
import UIKit
|
||||||
|
#endif
|
||||||
|
#if canImport(SwiftUI)
|
||||||
|
import SwiftUI
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Deprecated typealiases
|
||||||
|
@available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0")
|
||||||
|
internal typealias AssetColorTypeAlias = ColorAsset.Color
|
||||||
|
@available(*, deprecated, renamed: "ImageAsset.Image", message: "This typealias will be removed in SwiftGen 7.0")
|
||||||
|
internal typealias AssetImageTypeAlias = ImageAsset.Image
|
||||||
|
|
||||||
|
// swiftlint:disable superfluous_disable_command file_length implicit_return
|
||||||
|
|
||||||
|
// MARK: - Asset Catalogs
|
||||||
|
|
||||||
|
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
|
||||||
|
internal enum Asset {
|
||||||
|
internal static let back = ImageAsset(name: "Back")
|
||||||
|
internal static let betaBadge = ImageAsset(name: "BetaBadge")
|
||||||
|
internal static let accentColor = ColorAsset(name: "AccentColor")
|
||||||
|
internal static let background = ColorAsset(name: "Background")
|
||||||
|
internal static let blurTint = ColorAsset(name: "BlurTint")
|
||||||
|
internal static let settingsBackground = ColorAsset(name: "SettingsBackground")
|
||||||
|
internal static let settingsHighlighted = ColorAsset(name: "SettingsHighlighted")
|
||||||
|
internal static let next = ImageAsset(name: "Next")
|
||||||
|
internal static let riley = ImageAsset(name: "Riley")
|
||||||
|
internal static let shane = ImageAsset(name: "Shane")
|
||||||
|
internal static let browse = ImageAsset(name: "Browse")
|
||||||
|
internal static let myApps = ImageAsset(name: "MyApps")
|
||||||
|
internal static let news = ImageAsset(name: "News")
|
||||||
|
internal static let settings = ImageAsset(name: "Settings")
|
||||||
|
}
|
||||||
|
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
|
||||||
|
|
||||||
|
// MARK: - Implementation Details
|
||||||
|
|
||||||
|
internal final class ColorAsset {
|
||||||
|
internal fileprivate(set) var name: String
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
internal typealias Color = NSColor
|
||||||
|
#elseif os(iOS) || os(tvOS) || os(watchOS)
|
||||||
|
internal typealias Color = UIColor
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
|
||||||
|
internal private(set) lazy var color: Color = {
|
||||||
|
guard let color = Color(asset: self) else {
|
||||||
|
fatalError("Unable to load color asset named \(name).")
|
||||||
|
}
|
||||||
|
return color
|
||||||
|
}()
|
||||||
|
|
||||||
|
#if os(iOS) || os(tvOS)
|
||||||
|
@available(iOS 11.0, tvOS 11.0, *)
|
||||||
|
internal func color(compatibleWith traitCollection: UITraitCollection) -> Color {
|
||||||
|
let bundle = BundleToken.bundle
|
||||||
|
guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else {
|
||||||
|
fatalError("Unable to load color asset named \(name).")
|
||||||
|
}
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if canImport(SwiftUI)
|
||||||
|
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||||
|
internal private(set) lazy var swiftUIColor: SwiftUI.Color = {
|
||||||
|
SwiftUI.Color(asset: self)
|
||||||
|
}()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
fileprivate init(name: String) {
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal extension ColorAsset.Color {
|
||||||
|
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
|
||||||
|
convenience init?(asset: ColorAsset) {
|
||||||
|
let bundle = BundleToken.bundle
|
||||||
|
#if os(iOS) || os(tvOS)
|
||||||
|
self.init(named: asset.name, in: bundle, compatibleWith: nil)
|
||||||
|
#elseif os(macOS)
|
||||||
|
self.init(named: NSColor.Name(asset.name), bundle: bundle)
|
||||||
|
#elseif os(watchOS)
|
||||||
|
self.init(named: asset.name)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI)
|
||||||
|
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||||
|
internal extension SwiftUI.Color {
|
||||||
|
init(asset: ColorAsset) {
|
||||||
|
let bundle = BundleToken.bundle
|
||||||
|
self.init(asset.name, bundle: bundle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
internal struct ImageAsset {
|
||||||
|
internal fileprivate(set) var name: String
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
internal typealias Image = NSImage
|
||||||
|
#elseif os(iOS) || os(tvOS) || os(watchOS)
|
||||||
|
internal typealias Image = UIImage
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *)
|
||||||
|
internal var image: Image {
|
||||||
|
let bundle = BundleToken.bundle
|
||||||
|
#if os(iOS) || os(tvOS)
|
||||||
|
let image = Image(named: name, in: bundle, compatibleWith: nil)
|
||||||
|
#elseif os(macOS)
|
||||||
|
let name = NSImage.Name(self.name)
|
||||||
|
let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name)
|
||||||
|
#elseif os(watchOS)
|
||||||
|
let image = Image(named: name)
|
||||||
|
#endif
|
||||||
|
guard let result = image else {
|
||||||
|
fatalError("Unable to load image asset named \(name).")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS) || os(tvOS)
|
||||||
|
@available(iOS 8.0, tvOS 9.0, *)
|
||||||
|
internal func image(compatibleWith traitCollection: UITraitCollection) -> Image {
|
||||||
|
let bundle = BundleToken.bundle
|
||||||
|
guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else {
|
||||||
|
fatalError("Unable to load image asset named \(name).")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if canImport(SwiftUI)
|
||||||
|
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||||
|
internal var swiftUIImage: SwiftUI.Image {
|
||||||
|
SwiftUI.Image(asset: self)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
internal extension ImageAsset.Image {
|
||||||
|
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, *)
|
||||||
|
@available(macOS, deprecated,
|
||||||
|
message: "This initializer is unsafe on macOS, please use the ImageAsset.image property")
|
||||||
|
convenience init?(asset: ImageAsset) {
|
||||||
|
#if os(iOS) || os(tvOS)
|
||||||
|
let bundle = BundleToken.bundle
|
||||||
|
self.init(named: asset.name, in: bundle, compatibleWith: nil)
|
||||||
|
#elseif os(macOS)
|
||||||
|
self.init(named: NSImage.Name(asset.name))
|
||||||
|
#elseif os(watchOS)
|
||||||
|
self.init(named: asset.name)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI)
|
||||||
|
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||||
|
internal extension SwiftUI.Image {
|
||||||
|
init(asset: ImageAsset) {
|
||||||
|
let bundle = BundleToken.bundle
|
||||||
|
self.init(asset.name, bundle: bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(asset: ImageAsset, label: Text) {
|
||||||
|
let bundle = BundleToken.bundle
|
||||||
|
self.init(asset.name, bundle: bundle, label: label)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(decorative asset: ImageAsset) {
|
||||||
|
let bundle = BundleToken.bundle
|
||||||
|
self.init(decorative: asset.name, bundle: bundle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// swiftlint:disable convenience_type
|
||||||
|
private final class BundleToken {
|
||||||
|
static let bundle: Bundle = {
|
||||||
|
#if SWIFT_PACKAGE
|
||||||
|
return Bundle.module
|
||||||
|
#else
|
||||||
|
return Bundle(for: BundleToken.self)
|
||||||
|
#endif
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
// swiftlint:enable convenience_type
|
||||||
351
AltStore/Generated/Localizations.swift
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
// swiftlint:disable all
|
||||||
|
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// swiftlint:disable superfluous_disable_command file_length implicit_return prefer_self_in_static_references
|
||||||
|
|
||||||
|
// MARK: - Strings
|
||||||
|
|
||||||
|
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
||||||
|
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||||
|
internal enum L10n {
|
||||||
|
internal enum Action {
|
||||||
|
/// Close
|
||||||
|
internal static let close = L10n.tr("Localizable", "Action.close", fallback: "Close")
|
||||||
|
/// General Actions
|
||||||
|
internal static let done = L10n.tr("Localizable", "Action.done", fallback: "Done")
|
||||||
|
}
|
||||||
|
internal enum AddSourceView {
|
||||||
|
/// Continue
|
||||||
|
internal static let `continue` = L10n.tr("Localizable", "AddSourceView.continue", fallback: "Continue")
|
||||||
|
/// AddSourceView
|
||||||
|
internal static let sourceURL = L10n.tr("Localizable", "AddSourceView.sourceURL", fallback: "Source URL")
|
||||||
|
/// Please enter the source url here. Then, tap continue to validate and add the source in the next step.
|
||||||
|
internal static let sourceWarning = L10n.tr("Localizable", "AddSourceView.sourceWarning", fallback: "Please enter the source url here. Then, tap continue to validate and add the source in the next step.")
|
||||||
|
/// Be careful with unvalidated third-party sources! Make sure to only add sources that you trust.
|
||||||
|
internal static let sourceWarningContinued = L10n.tr("Localizable", "AddSourceView.sourceWarningContinued", fallback: "Be careful with unvalidated third-party sources! Make sure to only add sources that you trust.")
|
||||||
|
/// Add Source
|
||||||
|
internal static let title = L10n.tr("Localizable", "AddSourceView.title", fallback: "Add Source")
|
||||||
|
}
|
||||||
|
internal enum AppAction {
|
||||||
|
/// Activate
|
||||||
|
internal static let activate = L10n.tr("Localizable", "AppAction.activate", fallback: "Activate")
|
||||||
|
/// Backup
|
||||||
|
internal static let backup = L10n.tr("Localizable", "AppAction.backup", fallback: "Backup")
|
||||||
|
/// Customize icon
|
||||||
|
internal static let chooseCustomIcon = L10n.tr("Localizable", "AppAction.chooseCustomIcon", fallback: "Customize icon")
|
||||||
|
/// Deactivate
|
||||||
|
internal static let deactivate = L10n.tr("Localizable", "AppAction.deactivate", fallback: "Deactivate")
|
||||||
|
/// Activate JIT
|
||||||
|
internal static let enableJIT = L10n.tr("Localizable", "AppAction.enableJIT", fallback: "Activate JIT")
|
||||||
|
/// Export backup
|
||||||
|
internal static let exportBackup = L10n.tr("Localizable", "AppAction.exportBackup", fallback: "Export backup")
|
||||||
|
/// AppAction
|
||||||
|
internal static let install = L10n.tr("Localizable", "AppAction.install", fallback: "Install")
|
||||||
|
/// Open
|
||||||
|
internal static let `open` = L10n.tr("Localizable", "AppAction.open", fallback: "Open")
|
||||||
|
/// Refresh
|
||||||
|
internal static let refresh = L10n.tr("Localizable", "AppAction.refresh", fallback: "Refresh")
|
||||||
|
/// Remove
|
||||||
|
internal static let remove = L10n.tr("Localizable", "AppAction.remove", fallback: "Remove")
|
||||||
|
/// Reset icon
|
||||||
|
internal static let resetIcon = L10n.tr("Localizable", "AppAction.resetIcon", fallback: "Reset icon")
|
||||||
|
/// Restore backup
|
||||||
|
internal static let restoreBackup = L10n.tr("Localizable", "AppAction.restoreBackup", fallback: "Restore backup")
|
||||||
|
}
|
||||||
|
internal enum AppDetailView {
|
||||||
|
/// Information
|
||||||
|
internal static let information = L10n.tr("Localizable", "AppDetailView.information", fallback: "Information")
|
||||||
|
/// More...
|
||||||
|
internal static let more = L10n.tr("Localizable", "AppDetailView.more", fallback: "More...")
|
||||||
|
/// The app requires no permissions.
|
||||||
|
internal static let noPermissions = L10n.tr("Localizable", "AppDetailView.noPermissions", fallback: "The app requires no permissions.")
|
||||||
|
/// No screenshots available for this app.
|
||||||
|
internal static let noScreenshots = L10n.tr("Localizable", "AppDetailView.noScreenshots", fallback: "No screenshots available for this app.")
|
||||||
|
/// No version information
|
||||||
|
internal static let noVersionInformation = L10n.tr("Localizable", "AppDetailView.noVersionInformation", fallback: "No version information")
|
||||||
|
/// Permissions
|
||||||
|
internal static let permissions = L10n.tr("Localizable", "AppDetailView.permissions", fallback: "Permissions")
|
||||||
|
/// Ratings & Reviews
|
||||||
|
internal static let reviews = L10n.tr("Localizable", "AppDetailView.reviews", fallback: "Ratings & Reviews")
|
||||||
|
/// Version %@
|
||||||
|
internal static func version(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "AppDetailView.version", String(describing: p1), fallback: "Version %@")
|
||||||
|
}
|
||||||
|
/// What's New
|
||||||
|
internal static let whatsNew = L10n.tr("Localizable", "AppDetailView.whatsNew", fallback: "What's New")
|
||||||
|
internal enum Badge {
|
||||||
|
/// AppDetailView
|
||||||
|
internal static let official = L10n.tr("Localizable", "AppDetailView.Badge.official", fallback: "Official App")
|
||||||
|
/// From Trusted Source
|
||||||
|
internal static let trusted = L10n.tr("Localizable", "AppDetailView.Badge.trusted", fallback: "From Trusted Source")
|
||||||
|
}
|
||||||
|
internal enum Information {
|
||||||
|
/// Compatibility
|
||||||
|
internal static let compatibility = L10n.tr("Localizable", "AppDetailView.Information.compatibility", fallback: "Compatibility")
|
||||||
|
/// Requires iOS %@ or higher
|
||||||
|
internal static func compatibilityAtLeast(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "AppDetailView.Information.compatibilityAtLeast", String(describing: p1), fallback: "Requires iOS %@ or higher")
|
||||||
|
}
|
||||||
|
/// Unknown
|
||||||
|
internal static let compatibilityCompatible = L10n.tr("Localizable", "AppDetailView.Information.compatibilityCompatible", fallback: "Unknown")
|
||||||
|
/// Requires iOS %@ or lower
|
||||||
|
internal static func compatibilityOrLower(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "AppDetailView.Information.compatibilityOrLower", String(describing: p1), fallback: "Requires iOS %@ or lower")
|
||||||
|
}
|
||||||
|
/// Unknown
|
||||||
|
internal static let compatibilityUnknown = L10n.tr("Localizable", "AppDetailView.Information.compatibilityUnknown", fallback: "Unknown")
|
||||||
|
/// Developer
|
||||||
|
internal static let developer = L10n.tr("Localizable", "AppDetailView.Information.developer", fallback: "Developer")
|
||||||
|
/// Latest Version
|
||||||
|
internal static let latestVersion = L10n.tr("Localizable", "AppDetailView.Information.latestVersion", fallback: "Latest Version")
|
||||||
|
/// Size
|
||||||
|
internal static let size = L10n.tr("Localizable", "AppDetailView.Information.size", fallback: "Size")
|
||||||
|
/// Source
|
||||||
|
internal static let source = L10n.tr("Localizable", "AppDetailView.Information.source", fallback: "Source")
|
||||||
|
}
|
||||||
|
internal enum Reviews {
|
||||||
|
/// out of %d
|
||||||
|
internal static func outOf(_ p1: Int) -> String {
|
||||||
|
return L10n.tr("Localizable", "AppDetailView.Reviews.outOf", p1, fallback: "out of %d")
|
||||||
|
}
|
||||||
|
/// %d Ratings
|
||||||
|
internal static func ratings(_ p1: Int) -> String {
|
||||||
|
return L10n.tr("Localizable", "AppDetailView.Reviews.ratings", p1, fallback: "%d Ratings")
|
||||||
|
}
|
||||||
|
/// See All
|
||||||
|
internal static let seeAll = L10n.tr("Localizable", "AppDetailView.Reviews.seeAll", fallback: "See All")
|
||||||
|
}
|
||||||
|
internal enum WhatsNew {
|
||||||
|
/// Show project on GitHub
|
||||||
|
internal static let showOnGithub = L10n.tr("Localizable", "AppDetailView.WhatsNew.showOnGithub", fallback: "Show project on GitHub")
|
||||||
|
/// Version History
|
||||||
|
internal static let versionHistory = L10n.tr("Localizable", "AppDetailView.WhatsNew.versionHistory", fallback: "Version History")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
internal enum AppIDsView {
|
||||||
|
/// Each app and app extension installed with SideStore must register an App ID with Apple.
|
||||||
|
///
|
||||||
|
/// App IDs for paid developer accounts never expire, and there is no limit to how many you can create.
|
||||||
|
internal static let description = L10n.tr("Localizable", "AppIDsView.description", fallback: "Each app and app extension installed with SideStore must register an App ID with Apple.\n\nApp IDs for paid developer accounts never expire, and there is no limit to how many you can create.")
|
||||||
|
/// AppIDsView
|
||||||
|
internal static let title = L10n.tr("Localizable", "AppIDsView.title", fallback: "App IDs")
|
||||||
|
}
|
||||||
|
internal enum AppPermissionGrid {
|
||||||
|
/// AppPermissionGrid
|
||||||
|
internal static let usageDescription = L10n.tr("Localizable", "AppPermissionGrid.usageDescription", fallback: "Usage Description")
|
||||||
|
}
|
||||||
|
internal enum AppPillButton {
|
||||||
|
/// AppPillButton
|
||||||
|
internal static let free = L10n.tr("Localizable", "AppPillButton.free", fallback: "Free")
|
||||||
|
/// Open
|
||||||
|
internal static let `open` = L10n.tr("Localizable", "AppPillButton.open", fallback: "Open")
|
||||||
|
}
|
||||||
|
internal enum AppRowView {
|
||||||
|
/// AppRowView
|
||||||
|
internal static let sideloaded = L10n.tr("Localizable", "AppRowView.sideloaded", fallback: "Sideloaded")
|
||||||
|
}
|
||||||
|
internal enum BrowseView {
|
||||||
|
/// Search
|
||||||
|
internal static let search = L10n.tr("Localizable", "BrowseView.search", fallback: "Search")
|
||||||
|
/// BrowseView
|
||||||
|
internal static let title = L10n.tr("Localizable", "BrowseView.title", fallback: "Browse")
|
||||||
|
internal enum Actions {
|
||||||
|
/// Sources
|
||||||
|
internal static let sources = L10n.tr("Localizable", "BrowseView.Actions.sources", fallback: "Sources")
|
||||||
|
}
|
||||||
|
internal enum Categories {
|
||||||
|
/// Games and
|
||||||
|
/// Emulators
|
||||||
|
internal static let gamesAndEmulators = L10n.tr("Localizable", "BrowseView.Categories.gamesAndEmulators", fallback: "Games and\nEmulators")
|
||||||
|
}
|
||||||
|
internal enum Hints {
|
||||||
|
internal enum NoApps {
|
||||||
|
/// Add Source
|
||||||
|
internal static let addSource = L10n.tr("Localizable", "BrowseView.Hints.NoApps.addSource", fallback: "Add Source")
|
||||||
|
/// Apps are provided by "sources". The specification for them is an open standard, so everyone can create their own source. To get you started, we have compiled a list of "Trusted Sources" which you can check out by tapping the button below.
|
||||||
|
internal static let text = L10n.tr("Localizable", "BrowseView.Hints.NoApps.text", fallback: "Apps are provided by \"sources\". The specification for them is an open standard, so everyone can create their own source. To get you started, we have compiled a list of \"Trusted Sources\" which you can check out by tapping the button below.")
|
||||||
|
/// You don't have any apps yet.
|
||||||
|
internal static let title = L10n.tr("Localizable", "BrowseView.Hints.NoApps.title", fallback: "You don't have any apps yet.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
internal enum Section {
|
||||||
|
internal enum AllApps {
|
||||||
|
/// All Apps
|
||||||
|
internal static let title = L10n.tr("Localizable", "BrowseView.Section.AllApps.title", fallback: "All Apps")
|
||||||
|
}
|
||||||
|
internal enum PromotedCategories {
|
||||||
|
/// Show all
|
||||||
|
internal static let showAll = L10n.tr("Localizable", "BrowseView.Section.PromotedCategories.showAll", fallback: "Show all")
|
||||||
|
/// Promoted Categories
|
||||||
|
internal static let title = L10n.tr("Localizable", "BrowseView.Section.PromotedCategories.title", fallback: "Promoted Categories")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
internal enum ConfirmAddSourceView {
|
||||||
|
/// Add Source
|
||||||
|
internal static let addSource = L10n.tr("Localizable", "ConfirmAddSourceView.addSource", fallback: "Add Source")
|
||||||
|
/// ConfirmAddSourceView
|
||||||
|
internal static let apps = L10n.tr("Localizable", "ConfirmAddSourceView.apps", fallback: "Apps")
|
||||||
|
/// News Items
|
||||||
|
internal static let newsItems = L10n.tr("Localizable", "ConfirmAddSourceView.newsItems", fallback: "News Items")
|
||||||
|
/// Source Contents
|
||||||
|
internal static let sourceContents = L10n.tr("Localizable", "ConfirmAddSourceView.sourceContents", fallback: "Source Contents")
|
||||||
|
/// Source Identifier
|
||||||
|
internal static let sourceIdentifier = L10n.tr("Localizable", "ConfirmAddSourceView.sourceIdentifier", fallback: "Source Identifier")
|
||||||
|
/// Source Information
|
||||||
|
internal static let sourceInfo = L10n.tr("Localizable", "ConfirmAddSourceView.sourceInfo", fallback: "Source Information")
|
||||||
|
/// Source URL
|
||||||
|
internal static let sourceURL = L10n.tr("Localizable", "ConfirmAddSourceView.sourceURL", fallback: "Source URL")
|
||||||
|
}
|
||||||
|
internal enum ConnectAppleIDView {
|
||||||
|
/// Apple ID
|
||||||
|
internal static let appleID = L10n.tr("Localizable", "ConnectAppleIDView.appleID", fallback: "Apple ID")
|
||||||
|
/// Cancel
|
||||||
|
internal static let cancel = L10n.tr("Localizable", "ConnectAppleIDView.cancel", fallback: "Cancel")
|
||||||
|
/// Connect Your Apple ID
|
||||||
|
internal static let connectYourAppleID = L10n.tr("Localizable", "ConnectAppleIDView.connectYourAppleID", fallback: "Connect Your Apple ID")
|
||||||
|
/// Failed to Sign In
|
||||||
|
internal static let failedToSignIn = L10n.tr("Localizable", "ConnectAppleIDView.failedToSignIn", fallback: "Failed to Sign In")
|
||||||
|
/// Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.
|
||||||
|
internal static let footer = L10n.tr("Localizable", "ConnectAppleIDView.footer", fallback: "Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.")
|
||||||
|
/// Password
|
||||||
|
internal static let password = L10n.tr("Localizable", "ConnectAppleIDView.password", fallback: "Password")
|
||||||
|
/// Sign In
|
||||||
|
internal static let signIn = L10n.tr("Localizable", "ConnectAppleIDView.signIn", fallback: "Sign In")
|
||||||
|
/// ConnectAppleIDView
|
||||||
|
internal static let startWithSignIn = L10n.tr("Localizable", "ConnectAppleIDView.startWithSignIn", fallback: "Sign in with your Apple ID to get started.")
|
||||||
|
/// Why do we need this?
|
||||||
|
internal static let whyDoWeNeedThis = L10n.tr("Localizable", "ConnectAppleIDView.whyDoWeNeedThis", fallback: "Why do we need this?")
|
||||||
|
}
|
||||||
|
internal enum MyAppsView {
|
||||||
|
/// MyAppsView
|
||||||
|
internal static let active = L10n.tr("Localizable", "MyAppsView.active", fallback: "Active")
|
||||||
|
/// App IDs Remaining
|
||||||
|
internal static let appIDsRemaining = L10n.tr("Localizable", "MyAppsView.appIDsRemaining", fallback: "App IDs Remaining")
|
||||||
|
/// apps
|
||||||
|
internal static let apps = L10n.tr("Localizable", "MyAppsView.apps", fallback: "apps")
|
||||||
|
/// Failed to refresh
|
||||||
|
internal static let failedToRefresh = L10n.tr("Localizable", "MyAppsView.failedToRefresh", fallback: "Failed to refresh")
|
||||||
|
/// My Apps
|
||||||
|
internal static let myApps = L10n.tr("Localizable", "MyAppsView.myApps", fallback: "My Apps")
|
||||||
|
/// Refresh All
|
||||||
|
internal static let refreshAll = L10n.tr("Localizable", "MyAppsView.refreshAll", fallback: "Refresh All")
|
||||||
|
/// Sideloading in progress...
|
||||||
|
internal static let sideloading = L10n.tr("Localizable", "MyAppsView.sideloading", fallback: "Sideloading in progress...")
|
||||||
|
/// Keep this lowercase
|
||||||
|
internal static let viewAppIDs = L10n.tr("Localizable", "MyAppsView.viewAppIDs", fallback: "View App IDs")
|
||||||
|
internal enum Hints {
|
||||||
|
internal enum NoUpdates {
|
||||||
|
/// Dismiss for now
|
||||||
|
internal static let dismissForNow = L10n.tr("Localizable", "MyAppsView.Hints.NoUpdates.dismissForNow", fallback: "Dismiss for now")
|
||||||
|
/// Don't show this again
|
||||||
|
internal static let dontShowAgain = L10n.tr("Localizable", "MyAppsView.Hints.NoUpdates.dontShowAgain", fallback: "Don't show this again")
|
||||||
|
/// You will be notified once updates for your apps are available. The updates will then be shown here.
|
||||||
|
internal static let text = L10n.tr("Localizable", "MyAppsView.Hints.NoUpdates.text", fallback: "You will be notified once updates for your apps are available. The updates will then be shown here.")
|
||||||
|
/// All Apps are Up To Date
|
||||||
|
internal static let title = L10n.tr("Localizable", "MyAppsView.Hints.NoUpdates.title", fallback: "All Apps are Up To Date")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
internal enum NewsView {
|
||||||
|
/// NewsView
|
||||||
|
internal static let title = L10n.tr("Localizable", "NewsView.title", fallback: "News")
|
||||||
|
internal enum Section {
|
||||||
|
internal enum FromSources {
|
||||||
|
/// From your Sources
|
||||||
|
internal static let title = L10n.tr("Localizable", "NewsView.Section.FromSources.title", fallback: "From your Sources")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
internal enum RootView {
|
||||||
|
/// Browse
|
||||||
|
internal static let browse = L10n.tr("Localizable", "RootView.browse", fallback: "Browse")
|
||||||
|
/// My Apps
|
||||||
|
internal static let myApps = L10n.tr("Localizable", "RootView.myApps", fallback: "My Apps")
|
||||||
|
/// RootView
|
||||||
|
internal static let news = L10n.tr("Localizable", "RootView.news", fallback: "News")
|
||||||
|
/// Settings
|
||||||
|
internal static let settings = L10n.tr("Localizable", "RootView.settings", fallback: "Settings")
|
||||||
|
}
|
||||||
|
internal enum SettingsView {
|
||||||
|
/// Add to Siri...
|
||||||
|
internal static let addToSiri = L10n.tr("Localizable", "SettingsView.addToSiri", fallback: "Add to Siri...")
|
||||||
|
/// Background Refresh
|
||||||
|
internal static let backgroundRefresh = L10n.tr("Localizable", "SettingsView.backgroundRefresh", fallback: "Background Refresh")
|
||||||
|
/// Connect your Apple ID
|
||||||
|
internal static let connectAppleID = L10n.tr("Localizable", "SettingsView.connectAppleID", fallback: "Connect your Apple ID")
|
||||||
|
/// Credits
|
||||||
|
internal static let credits = L10n.tr("Localizable", "SettingsView.credits", fallback: "Credits")
|
||||||
|
/// Debug
|
||||||
|
internal static let debug = L10n.tr("Localizable", "SettingsView.debug", fallback: "Debug")
|
||||||
|
/// Refreshing Apps
|
||||||
|
internal static let refreshingApps = L10n.tr("Localizable", "SettingsView.refreshingApps", fallback: "Refreshing Apps")
|
||||||
|
/// Enable Background Refresh to automatically refresh apps in the background when connected to WiFi and with Wireguard active.
|
||||||
|
internal static let refreshingAppsFooter = L10n.tr("Localizable", "SettingsView.refreshingAppsFooter", fallback: "Enable Background Refresh to automatically refresh apps in the background when connected to WiFi and with Wireguard active.")
|
||||||
|
/// Reset Image Cache
|
||||||
|
internal static let resetImageCache = L10n.tr("Localizable", "SettingsView.resetImageCache", fallback: "Reset Image Cache")
|
||||||
|
/// SwiftUI Redesign
|
||||||
|
internal static let swiftUIRedesign = L10n.tr("Localizable", "SettingsView.swiftUIRedesign", fallback: "SwiftUI Redesign")
|
||||||
|
/// Switch to UIKit
|
||||||
|
internal static let switchToUIKit = L10n.tr("Localizable", "SettingsView.switchToUIKit", fallback: "Switch to UIKit")
|
||||||
|
/// Settings
|
||||||
|
internal static let title = L10n.tr("Localizable", "SettingsView.title", fallback: "Settings")
|
||||||
|
internal enum ConnectedAppleID {
|
||||||
|
/// E-Mail
|
||||||
|
internal static let eMail = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.eMail", fallback: "E-Mail")
|
||||||
|
/// SettingsView
|
||||||
|
internal static let name = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.name", fallback: "Name")
|
||||||
|
/// Sign Out
|
||||||
|
internal static let signOut = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.signOut", fallback: "Sign Out")
|
||||||
|
/// Connected Apple ID
|
||||||
|
internal static let text = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.text", fallback: "Connected Apple ID")
|
||||||
|
/// Type
|
||||||
|
internal static let type = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.type", fallback: "Type")
|
||||||
|
internal enum Footer {
|
||||||
|
/// Your Apple ID is required to sign the apps you install with SideStore.
|
||||||
|
internal static let p1 = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.Footer.p1", fallback: "Your Apple ID is required to sign the apps you install with SideStore.")
|
||||||
|
/// Your credentials are only sent to Apple's servers and are not accessible by the SideStore Team. Once successfully logged in, the login details are stored securely on your device.
|
||||||
|
internal static let p2 = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.Footer.p2", fallback: "Your credentials are only sent to Apple's servers and are not accessible by the SideStore Team. Once successfully logged in, the login details are stored securely on your device.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
internal enum SourcesView {
|
||||||
|
/// Done
|
||||||
|
internal static let done = L10n.tr("Localizable", "SourcesView.done", fallback: "Done")
|
||||||
|
/// Remove
|
||||||
|
internal static let remove = L10n.tr("Localizable", "SourcesView.remove", fallback: "Remove")
|
||||||
|
/// SideStore has reviewed these sources to make sure they meet our safety standards.
|
||||||
|
internal static let reviewedText = L10n.tr("Localizable", "SourcesView.reviewedText", fallback: "SideStore has reviewed these sources to make sure they meet our safety standards.")
|
||||||
|
/// Sources
|
||||||
|
internal static let sources = L10n.tr("Localizable", "SourcesView.sources", fallback: "Sources")
|
||||||
|
/// SourcesView
|
||||||
|
internal static let sourcesDescription = L10n.tr("Localizable", "SourcesView.sourcesDescription", fallback: "Sources control what apps are available to download through SideStore.")
|
||||||
|
/// Trusted Sources
|
||||||
|
internal static let trustedSources = L10n.tr("Localizable", "SourcesView.trustedSources", fallback: "Trusted Sources")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
|
||||||
|
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||||
|
|
||||||
|
// MARK: - Implementation Details
|
||||||
|
|
||||||
|
extension L10n {
|
||||||
|
private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String {
|
||||||
|
let format = BundleToken.bundle.localizedString(forKey: key, value: value, table: table)
|
||||||
|
return String(format: format, locale: Locale.current, arguments: args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable convenience_type
|
||||||
|
private final class BundleToken {
|
||||||
|
static let bundle: Bundle = {
|
||||||
|
#if SWIFT_PACKAGE
|
||||||
|
return Bundle.module
|
||||||
|
#else
|
||||||
|
return Bundle(for: BundleToken.self)
|
||||||
|
#endif
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
// swiftlint:enable convenience_type
|
||||||
72
AltStore/Helper/DateFormatterHelper.swift
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
//
|
||||||
|
// DateFormatterHelper.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 20.11.22.
|
||||||
|
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct DateFormatterHelper {
|
||||||
|
|
||||||
|
private static let appExpirationDateFormatter: DateComponentsFormatter = {
|
||||||
|
let dateComponentsFormatter = DateComponentsFormatter()
|
||||||
|
dateComponentsFormatter.zeroFormattingBehavior = [.pad]
|
||||||
|
dateComponentsFormatter.collapsesLargestUnit = false
|
||||||
|
return dateComponentsFormatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static let relativeDateFormatter: RelativeDateTimeFormatter = {
|
||||||
|
let formatter = RelativeDateTimeFormatter()
|
||||||
|
formatter.unitsStyle = .abbreviated
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static let mediumDateFormatter: DateFormatter = {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateStyle = .medium
|
||||||
|
dateFormatter.timeStyle = .none
|
||||||
|
dateFormatter.doesRelativeDateFormatting = true
|
||||||
|
return dateFormatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static let timeFormatter: DateFormatter = {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateStyle = .none
|
||||||
|
dateFormatter.timeStyle = .short
|
||||||
|
return dateFormatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static func string(forExpirationDate date: Date) -> String {
|
||||||
|
let startDate = Date()
|
||||||
|
let interval = date.timeIntervalSince(startDate)
|
||||||
|
guard interval > 0 else {
|
||||||
|
return "EXPIRED"
|
||||||
|
}
|
||||||
|
|
||||||
|
if interval < (24 * 60 * 60) {
|
||||||
|
self.appExpirationDateFormatter.unitsStyle = .positional
|
||||||
|
self.appExpirationDateFormatter.allowedUnits = [.minute, .second]
|
||||||
|
} else {
|
||||||
|
self.appExpirationDateFormatter.unitsStyle = .full
|
||||||
|
self.appExpirationDateFormatter.allowedUnits = [.day]
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.appExpirationDateFormatter.string(from: startDate, to: date) ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
static func string(forRelativeDate date: Date, to referenceDate: Date = Date()) -> String {
|
||||||
|
self.relativeDateFormatter.localizedString(for: date, relativeTo: referenceDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func string(for date: Date) -> String {
|
||||||
|
self.mediumDateFormatter.string(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func timeString(for date: Date) -> String {
|
||||||
|
self.timeFormatter.string(from: date)
|
||||||
|
}
|
||||||
|
}
|
||||||
254
AltStore/Helper/SideloadingManager.swift
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
//
|
||||||
|
// SideloadingManager.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 20.12.22.
|
||||||
|
// Copyright © 2022 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import CoreData
|
||||||
|
import AltStoreCore
|
||||||
|
import CAltSign
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
// TODO: Move this to the AppManager
|
||||||
|
class SideloadingManager {
|
||||||
|
class Context {
|
||||||
|
var fileURL: URL?
|
||||||
|
var application: ALTApplication?
|
||||||
|
var installedApp: InstalledApp? {
|
||||||
|
didSet {
|
||||||
|
self.installedAppContext = self.installedApp?.managedObjectContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var installedAppContext: NSManagedObjectContext?
|
||||||
|
var error: Error?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static let shared = SideloadingManager()
|
||||||
|
|
||||||
|
@Published
|
||||||
|
public var progress: Progress?
|
||||||
|
|
||||||
|
private let operationQueue = OperationQueue()
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: Refactor & convert to async
|
||||||
|
func sideloadApp(at url: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
self.progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
|
|
||||||
|
let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||||
|
let unzippedAppDirectory = temporaryDirectory.appendingPathComponent("App")
|
||||||
|
|
||||||
|
let context = Context()
|
||||||
|
|
||||||
|
let downloadOperation: RSTAsyncBlockOperation?
|
||||||
|
|
||||||
|
if url.isFileURL {
|
||||||
|
downloadOperation = nil
|
||||||
|
context.fileURL = url
|
||||||
|
self.progress?.totalUnitCount -= 20
|
||||||
|
} else {
|
||||||
|
let downloadProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
|
|
||||||
|
downloadOperation = RSTAsyncBlockOperation { (operation) in
|
||||||
|
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||||
|
|
||||||
|
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
|
let destinationURL = temporaryDirectory.appendingPathComponent("App.ipa")
|
||||||
|
try FileManager.default.moveItem(at: fileURL, to: destinationURL)
|
||||||
|
|
||||||
|
context.fileURL = destinationURL
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
context.error = error
|
||||||
|
}
|
||||||
|
operation.finish()
|
||||||
|
}
|
||||||
|
downloadProgress.addChild(downloadTask.progress, withPendingUnitCount: 100)
|
||||||
|
downloadTask.resume()
|
||||||
|
}
|
||||||
|
self.progress?.addChild(downloadProgress, withPendingUnitCount: 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
let unzipProgress = Progress.discreteProgress(totalUnitCount: 1)
|
||||||
|
let unzipAppOperation = BlockOperation {
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if let error = context.error
|
||||||
|
{
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let fileURL = context.fileURL else { throw OperationError.invalidParameters }
|
||||||
|
defer {
|
||||||
|
try? FileManager.default.removeItem(at: fileURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
try FileManager.default.createDirectory(at: unzippedAppDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: unzippedAppDirectory)
|
||||||
|
|
||||||
|
guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { throw OperationError.invalidApp }
|
||||||
|
context.application = application
|
||||||
|
|
||||||
|
unzipProgress.completedUnitCount = 1
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
context.error = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.progress?.addChild(unzipProgress, withPendingUnitCount: 10)
|
||||||
|
|
||||||
|
if let downloadOperation = downloadOperation
|
||||||
|
{
|
||||||
|
unzipAppOperation.addDependency(downloadOperation)
|
||||||
|
}
|
||||||
|
|
||||||
|
let removeAppExtensionsProgress = Progress.discreteProgress(totalUnitCount: 1)
|
||||||
|
|
||||||
|
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if let error = context.error
|
||||||
|
{
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let application = context.application else { throw OperationError.invalidParameters }
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.removeAppExtensions(from: application) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .success: removeAppExtensionsProgress.completedUnitCount = 1
|
||||||
|
case .failure(let error): context.error = error
|
||||||
|
}
|
||||||
|
operation.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
context.error = error
|
||||||
|
operation.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeAppExtensionsOperation.addDependency(unzipAppOperation)
|
||||||
|
self.progress?.addChild(removeAppExtensionsProgress, withPendingUnitCount: 5)
|
||||||
|
|
||||||
|
let installProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
|
let installAppOperation = RSTAsyncBlockOperation { (operation) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if let error = context.error
|
||||||
|
{
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let application = context.application else { throw OperationError.invalidParameters }
|
||||||
|
|
||||||
|
let group = AppManager.shared.install(application, presentingViewController: nil) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .success(let installedApp): context.installedApp = installedApp
|
||||||
|
case .failure(let error): context.error = error
|
||||||
|
}
|
||||||
|
operation.finish()
|
||||||
|
}
|
||||||
|
installProgress.addChild(group.progress, withPendingUnitCount: 100)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
context.error = error
|
||||||
|
operation.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
installAppOperation.completionBlock = {
|
||||||
|
try? FileManager.default.removeItem(at: temporaryDirectory)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.progress = nil
|
||||||
|
|
||||||
|
switch Result(context.installedApp, context.error)
|
||||||
|
{
|
||||||
|
case .success(let app):
|
||||||
|
completion(.success(()))
|
||||||
|
|
||||||
|
app.managedObjectContext?.perform {
|
||||||
|
print("Successfully installed app:", app.bundleIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(OperationError.cancelled):
|
||||||
|
completion(.failure((OperationError.cancelled)))
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
NotificationManager.shared.reportError(error: error)
|
||||||
|
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.progress?.addChild(installProgress, withPendingUnitCount: 65)
|
||||||
|
installAppOperation.addDependency(removeAppExtensionsOperation)
|
||||||
|
|
||||||
|
let operations = [downloadOperation, unzipAppOperation, removeAppExtensionsOperation, installAppOperation].compactMap { $0 }
|
||||||
|
self.operationQueue.addOperations(operations, waitUntilFinished: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: Refactor
|
||||||
|
private func removeAppExtensions(from application: ALTApplication, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
|
guard !application.appExtensions.isEmpty else { return completion(.success(())) }
|
||||||
|
|
||||||
|
let firstSentence: String
|
||||||
|
|
||||||
|
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
||||||
|
{
|
||||||
|
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit?", comment: "")
|
||||||
|
|
||||||
|
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
|
||||||
|
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
|
||||||
|
completion(.failure(OperationError.cancelled))
|
||||||
|
}))
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
|
||||||
|
completion(.success(()))
|
||||||
|
})
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
for appExtension in application.appExtensions
|
||||||
|
{
|
||||||
|
try FileManager.default.removeItem(at: appExtension.fileURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let rootViewController = UIApplication.shared.keyWindow?.rootViewController
|
||||||
|
rootViewController?.present(alertController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<key>ALTAppGroups</key>
|
<key>ALTAppGroups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||||
<string>group.com.rileytestut.AltStore</string>
|
<string>group.com.SideStore.SideStore</string>
|
||||||
</array>
|
</array>
|
||||||
<key>ALTDeviceID</key>
|
<key>ALTDeviceID</key>
|
||||||
<string>00008101-000129D63698001E</string>
|
<string>00008101-000129D63698001E</string>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<key>ALTPairingFile</key>
|
<key>ALTPairingFile</key>
|
||||||
<string><insert pairing file here></string>
|
<string><insert pairing file here></string>
|
||||||
<key>ALTAnisetteURL</key>
|
<key>ALTAnisetteURL</key>
|
||||||
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string>
|
<string>https://ani.sidestore.io</string>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDocumentTypes</key>
|
<key>CFBundleDocumentTypes</key>
|
||||||
@@ -44,6 +44,8 @@
|
|||||||
<string>$(PRODUCT_NAME)</string>
|
<string>$(PRODUCT_NAME)</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
<true/>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -56,6 +58,7 @@
|
|||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>altstore</string>
|
<string>altstore</string>
|
||||||
|
<string>sidestore</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -66,6 +69,7 @@
|
|||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>altstore-com.rileytestut.AltStore</string>
|
<string>altstore-com.rileytestut.AltStore</string>
|
||||||
|
<string>sidestore-com.SideStore.SideStore</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
@@ -200,5 +204,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIFileSharingEnabled</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Foundation
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
class IntentHandler: NSObject, RefreshAllIntentHandling
|
final class IntentHandler: NSObject, RefreshAllIntentHandling
|
||||||
{
|
{
|
||||||
private let queue = DispatchQueue(label: "io.altstore.IntentHandler")
|
private let queue = DispatchQueue(label: "io.altstore.IntentHandler")
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
import Roxas
|
import Roxas
|
||||||
import EmotionalDamage
|
import EmotionalDamage
|
||||||
import minimuxer
|
import minimuxer
|
||||||
@@ -14,7 +15,9 @@ import minimuxer
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
let pairingFileName = "ALTPairingFile.mobiledevicepairing"
|
||||||
|
|
||||||
|
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
||||||
{
|
{
|
||||||
private var didFinishLaunching = false
|
private var didFinishLaunching = false
|
||||||
|
|
||||||
@@ -40,20 +43,40 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
|||||||
{
|
{
|
||||||
defer {
|
defer {
|
||||||
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
||||||
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
// self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
||||||
|
let rootView = RootView()
|
||||||
|
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
|
||||||
|
self.destinationViewController = UIHostingController(rootView: rootView)
|
||||||
}
|
}
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(true)
|
super.viewDidAppear(true)
|
||||||
|
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
|
if !UserDefaults.standard.onboardingComplete {
|
||||||
|
self.showOnboarding()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||||
|
|
||||||
guard let pf = fetchPairingFile() else {
|
guard let pf = fetchPairingFile() else {
|
||||||
displayError("Device pairing file not found.")
|
self.showOnboarding(enabledSteps: [.pairing])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
start_minimuxer_threads(pf)
|
start_minimuxer_threads(pf)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func showOnboarding(enabledSteps: [OnboardingStep] = OnboardingStep.allCases) {
|
||||||
|
let onboardingView = OnboardingView(onDismiss: { self.dismiss(animated: true) }, enabledSteps: enabledSteps)
|
||||||
|
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
|
||||||
|
let navigationController = UINavigationController(rootViewController: UIHostingController(rootView: onboardingView))
|
||||||
|
navigationController.isNavigationBarHidden = true
|
||||||
|
navigationController.isModalInPresentation = true
|
||||||
|
self.present(navigationController, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchPairingFile() -> String? {
|
func fetchPairingFile() -> String? {
|
||||||
@@ -77,14 +100,16 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
|||||||
} else {
|
} else {
|
||||||
// Show an alert explaining the pairing file
|
// Show an alert explaining the pairing file
|
||||||
// Create new Alert
|
// Create new Alert
|
||||||
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://youtu.be/dQw4w9WgXcQ", preferredStyle: .alert)
|
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://wiki.sidestore.io/guides/install#pairing-process", preferredStyle: .alert)
|
||||||
|
|
||||||
// Create OK button with action handler
|
// Create OK button with action handler
|
||||||
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
|
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
|
||||||
// Try to load it from a file picker
|
// Try to load it from a file picker
|
||||||
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
|
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
|
||||||
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: nil))
|
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
|
||||||
|
types.append(.xml)
|
||||||
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
|
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
|
||||||
|
documentPickerController.shouldShowFileExtensions = true
|
||||||
documentPickerController.delegate = self
|
documentPickerController.delegate = self
|
||||||
self.present(documentPickerController, animated: true, completion: nil)
|
self.present(documentPickerController, animated: true, completion: nil)
|
||||||
})
|
})
|
||||||
@@ -121,14 +146,11 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save to a file for next launch
|
// Save to a file for next launch
|
||||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
let pairingFile = FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")
|
||||||
let fm = FileManager.default
|
try pairing_string?.write(to: pairingFile, atomically: true, encoding: String.Encoding.utf8)
|
||||||
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
|
||||||
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
|
|
||||||
|
|
||||||
// Start minimuxer now that we have a file
|
// Start minimuxer now that we have a file
|
||||||
start_minimuxer_threads(pairing_string!)
|
start_minimuxer_threads(pairing_string!)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
displayError("Unable to read pairing file")
|
displayError("Unable to read pairing file")
|
||||||
}
|
}
|
||||||
@@ -140,16 +162,19 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
|||||||
}
|
}
|
||||||
|
|
||||||
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||||
displayError("Choosing a pairing file was cancelled")
|
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func start_minimuxer_threads(_ pairing_file: String) {
|
func start_minimuxer_threads(_ pairing_file: String) {
|
||||||
set_usbmuxd_socket()
|
target_minimuxer_address()
|
||||||
let res = start_minimuxer(pairing_file: pairing_file)
|
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||||
if res != 0 {
|
do {
|
||||||
displayError("minimuxer failed to start. Incorrect arguments were passed.")
|
try start(pairing_file, documentsDirectory)
|
||||||
|
} catch {
|
||||||
|
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
|
||||||
|
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
|
||||||
}
|
}
|
||||||
auto_mount_dev_image()
|
start_auto_mounter(documentsDirectory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +190,19 @@ extension LaunchViewController
|
|||||||
{
|
{
|
||||||
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
|
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
|
||||||
|
|
||||||
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert)
|
let errorDescription: String
|
||||||
|
|
||||||
|
if #available(iOS 14.5, *)
|
||||||
|
{
|
||||||
|
let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
|
||||||
|
errorDescription = errorMessages.joined(separator: "\n\n")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errorDescription = error.debugDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert)
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
|
||||||
self.handleLaunchConditions()
|
self.handleLaunchConditions()
|
||||||
}))
|
}))
|
||||||
|
|||||||
80
AltStore/Manager/NotificationManager.swift
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
//
|
||||||
|
// NotificationManager.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 21.11.22.
|
||||||
|
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
class NotificationManager: ObservableObject {
|
||||||
|
|
||||||
|
struct Notification: Identifiable {
|
||||||
|
let id: UUID
|
||||||
|
let title: String
|
||||||
|
let message: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
static let shared = NotificationManager()
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var notifications: [UUID: Notification] = [:]
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func reportError(error: Error) {
|
||||||
|
if case OperationError.cancelled = error {
|
||||||
|
// Ignore
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var error = error as NSError
|
||||||
|
var underlyingError = error.underlyingError
|
||||||
|
|
||||||
|
if
|
||||||
|
let unwrappedUnderlyingError = underlyingError,
|
||||||
|
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
|
||||||
|
{
|
||||||
|
// Treat underlyingError as the primary error.
|
||||||
|
|
||||||
|
error = unwrappedUnderlyingError as NSError
|
||||||
|
underlyingError = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
let detailText: String?
|
||||||
|
|
||||||
|
if let failure = error.localizedFailure
|
||||||
|
{
|
||||||
|
text = failure
|
||||||
|
detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription
|
||||||
|
}
|
||||||
|
else if let reason = error.localizedFailureReason
|
||||||
|
{
|
||||||
|
text = reason
|
||||||
|
detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text = error.localizedDescription
|
||||||
|
detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion
|
||||||
|
}
|
||||||
|
|
||||||
|
self.showNotification(title: text, detailText: detailText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showNotification(title: String, detailText: String? = nil) {
|
||||||
|
let notificationId = UUID()
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.notifications[notificationId] = Notification(id: notificationId, title: title, message: detailText)
|
||||||
|
}
|
||||||
|
|
||||||
|
let dismissWorkItem = DispatchWorkItem {
|
||||||
|
self.notifications.removeValue(forKey: notificationId)
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now().advanced(by: .seconds(5)), execute: dismissWorkItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
56
AltStore/Manager/OutputCapturer.swift
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
//
|
||||||
|
// OutputCapturer.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 12.02.23.
|
||||||
|
// Copyright © 2023 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import LocalConsole
|
||||||
|
|
||||||
|
class OutputCapturer {
|
||||||
|
|
||||||
|
public static let shared = OutputCapturer()
|
||||||
|
|
||||||
|
private let consoleManager = LCManager.shared
|
||||||
|
|
||||||
|
private var inputPipe = Pipe()
|
||||||
|
private var errorPipe = Pipe()
|
||||||
|
private var outputPipe = Pipe()
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
// Setup pipe file handlers
|
||||||
|
self.inputPipe.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in
|
||||||
|
self?.handle(data: fileHandle.availableData)
|
||||||
|
}
|
||||||
|
self.errorPipe.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in
|
||||||
|
self?.handle(data: fileHandle.availableData, isError: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep STDOUT
|
||||||
|
dup2(STDOUT_FILENO, self.outputPipe.fileHandleForWriting.fileDescriptor)
|
||||||
|
|
||||||
|
// Intercept STDOUT and STDERR
|
||||||
|
dup2(self.inputPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO)
|
||||||
|
dup2(self.errorPipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
try? self.inputPipe.fileHandleForReading.close()
|
||||||
|
try? self.errorPipe.fileHandleForReading.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handle(data: Data, isError: Bool = false) {
|
||||||
|
// Write output to STDOUT
|
||||||
|
self.outputPipe.fileHandleForWriting.write(data)
|
||||||
|
|
||||||
|
guard let string = String(data: data, encoding: .utf8) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.consoleManager.print(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ extension AppManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 13, *)
|
@available(iOS 13, *)
|
||||||
class AppManagerPublisher: ObservableObject
|
final class AppManagerPublisher: ObservableObject
|
||||||
{
|
{
|
||||||
@Published
|
@Published
|
||||||
fileprivate(set) var installationProgress = [String: Progress]()
|
fileprivate(set) var installationProgress = [String: Progress]()
|
||||||
@@ -42,7 +42,7 @@ private func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Boo
|
|||||||
return (lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion)
|
return (lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppManager
|
final class AppManager
|
||||||
{
|
{
|
||||||
static let shared = AppManager()
|
static let shared = AppManager()
|
||||||
|
|
||||||
@@ -392,7 +392,8 @@ extension AppManager
|
|||||||
func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void)
|
func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void)
|
||||||
{
|
{
|
||||||
let authenticationOperation = self.authenticate(presentingViewController: nil) { (result) in
|
let authenticationOperation = self.authenticate(presentingViewController: nil) { (result) in
|
||||||
print("Authenticated for fetching App IDs with result:", result)
|
// result contains name, email, auth token, OTP and other possibly personal/account specific info. we don't want this logged
|
||||||
|
//print("Authenticated for fetching App IDs with result:", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
let fetchAppIDsOperation = FetchAppIDsOperation(context: authenticationOperation.context)
|
let fetchAppIDsOperation = FetchAppIDsOperation(context: authenticationOperation.context)
|
||||||
@@ -664,7 +665,7 @@ extension AppManager
|
|||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
func enableJIT(for installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
func enableJIT(for installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
{
|
{
|
||||||
class Context: OperationContext, EnableJITContext
|
final class Context: OperationContext, EnableJITContext
|
||||||
{
|
{
|
||||||
var installedApp: InstalledApp?
|
var installedApp: InstalledApp?
|
||||||
}
|
}
|
||||||
@@ -684,7 +685,7 @@ extension AppManager
|
|||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
func patch(resignedApp: ALTApplication, presentingViewController: UIViewController, context authContext: AuthenticatedOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> PatchAppOperation
|
func patch(resignedApp: ALTApplication, presentingViewController: UIViewController, context authContext: AuthenticatedOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> PatchAppOperation
|
||||||
{
|
{
|
||||||
class Context: InstallAppOperationContext, PatchAppContext
|
final class Context: InstallAppOperationContext, PatchAppContext
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -758,7 +759,7 @@ extension AppManager
|
|||||||
extension AppManager
|
extension AppManager
|
||||||
{
|
{
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func backgroundRefresh(_ installedApps: [InstalledApp], presentsNotifications: Bool = true, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) -> BackgroundRefreshAppsOperation
|
func backgroundRefresh(_ installedApps: [InstalledApp], presentsNotifications: Bool = false, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) -> BackgroundRefreshAppsOperation
|
||||||
{
|
{
|
||||||
let backgroundRefreshAppsOperation = BackgroundRefreshAppsOperation(installedApps: installedApps)
|
let backgroundRefreshAppsOperation = BackgroundRefreshAppsOperation(installedApps: installedApps)
|
||||||
backgroundRefreshAppsOperation.resultHandler = completionHandler
|
backgroundRefreshAppsOperation.resultHandler = completionHandler
|
||||||
@@ -875,7 +876,9 @@ private extension AppManager
|
|||||||
|
|
||||||
if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
|
if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
|
||||||
uti != nil ||
|
uti != nil ||
|
||||||
app.needsResign
|
app.needsResign ||
|
||||||
|
// We need to reinstall ourselves on refresh to ensure the new provisioning profile is used
|
||||||
|
app.bundleIdentifier == StoreApp.altstoreAppID
|
||||||
{
|
{
|
||||||
// Resign app instead of just refreshing profiles because either:
|
// Resign app instead of just refreshing profiles because either:
|
||||||
// * Refreshing using different certificate
|
// * Refreshing using different certificate
|
||||||
@@ -1121,7 +1124,7 @@ private extension AppManager
|
|||||||
presentingViewController?.dismiss(animated: true, completion: nil)
|
presentingViewController?.dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
presentingViewController.present(navigationController, animated: true, completion: nil)
|
presentingViewController.present(navigationController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -1223,7 +1226,7 @@ private extension AppManager
|
|||||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
|
|
||||||
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||||
context.app = ALTApplication(fileURL: app.url)
|
context.app = ALTApplication(fileURL: app.fileURL)
|
||||||
|
|
||||||
/* Fetch Provisioning Profiles */
|
/* Fetch Provisioning Profiles */
|
||||||
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
||||||
@@ -1662,13 +1665,8 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
if #available(iOS 14, *)
|
if #available(iOS 14, *)
|
||||||
{
|
{
|
||||||
WidgetCenter.shared.getCurrentConfigurations { (result) in
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
guard case .success(let widgets) = result else { return }
|
|
||||||
|
|
||||||
guard let widget = widgets.first(where: { $0.configuration is ViewAppIntent }) else { return }
|
|
||||||
WidgetCenter.shared.reloadTimelines(ofKind: widget.kind)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
do { try installedApp.managedObjectContext?.save() }
|
do { try installedApp.managedObjectContext?.save() }
|
||||||
@@ -1677,6 +1675,8 @@ private extension AppManager
|
|||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
|
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
|
||||||
|
|
||||||
|
self.log(error, for: operation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1701,6 +1701,43 @@ private extension AppManager
|
|||||||
UNUserNotificationCenter.current().add(request)
|
UNUserNotificationCenter.current().add(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func log(_ error: Error, for operation: AppOperation)
|
||||||
|
{
|
||||||
|
// Sanitize NSError on same thread before performing background task.
|
||||||
|
let sanitizedError = (error as NSError).sanitizedForCoreData()
|
||||||
|
|
||||||
|
let loggedErrorOperation: LoggedError.Operation = {
|
||||||
|
switch operation
|
||||||
|
{
|
||||||
|
case .install: return .install
|
||||||
|
case .update: return .update
|
||||||
|
case .refresh: return .refresh
|
||||||
|
case .activate: return .activate
|
||||||
|
case .deactivate: return .deactivate
|
||||||
|
case .backup: return .backup
|
||||||
|
case .restore: return .restore
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
||||||
|
var app = operation.app
|
||||||
|
if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol
|
||||||
|
{
|
||||||
|
app = tempApp
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
_ = LoggedError(error: sanitizedError, app: app, operation: loggedErrorOperation, context: context)
|
||||||
|
try context.save()
|
||||||
|
}
|
||||||
|
catch let saveError
|
||||||
|
{
|
||||||
|
print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false)
|
func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false)
|
||||||
{
|
{
|
||||||
// Find "Install AltStore" operation if it already exists in `context`
|
// Find "Install AltStore" operation if it already exists in `context`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class InstalledAppsCollectionHeaderView: UICollectionReusableView
|
final class InstalledAppsCollectionHeaderView: UICollectionReusableView
|
||||||
{
|
{
|
||||||
let textLabel: UILabel
|
let textLabel: UILabel
|
||||||
let button: UIButton
|
let button: UIButton
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
class InstalledAppCollectionViewCell: UICollectionViewCell
|
final class InstalledAppCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
private(set) var deactivateBadge: UIView?
|
private(set) var deactivateBadge: UIView?
|
||||||
|
|
||||||
@@ -55,13 +55,13 @@ class InstalledAppCollectionViewCell: UICollectionViewCell
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InstalledAppsCollectionFooterView: UICollectionReusableView
|
final class InstalledAppsCollectionFooterView: UICollectionReusableView
|
||||||
{
|
{
|
||||||
@IBOutlet var textLabel: UILabel!
|
@IBOutlet var textLabel: UILabel!
|
||||||
@IBOutlet var button: UIButton!
|
@IBOutlet var button: UIButton!
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoUpdatesCollectionViewCell: UICollectionViewCell
|
final class NoUpdatesCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
@IBOutlet var blurView: UIVisualEffectView!
|
@IBOutlet var blurView: UIVisualEffectView!
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ class NoUpdatesCollectionViewCell: UICollectionViewCell
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpdatesCollectionHeaderView: UICollectionReusableView
|
final class UpdatesCollectionHeaderView: UICollectionReusableView
|
||||||
{
|
{
|
||||||
let button = PillButton(type: .system)
|
let button = PillButton(type: .system)
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyAppsViewController: UICollectionViewController
|
final class MyAppsViewController: UICollectionViewController
|
||||||
{
|
{
|
||||||
private let coordinator = NSFileCoordinator()
|
private let coordinator = NSFileCoordinator()
|
||||||
private let operationQueue = OperationQueue()
|
private let operationQueue = OperationQueue()
|
||||||
@@ -186,7 +186,7 @@ private extension MyAppsViewController
|
|||||||
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
|
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
|
||||||
{
|
{
|
||||||
let fetchRequest = InstalledApp.updatesFetchRequest()
|
let fetchRequest = InstalledApp.updatesFetchRequest()
|
||||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.versionDate, ascending: true),
|
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestVersion?.date, ascending: true),
|
||||||
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
|
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
|
||||||
fetchRequest.returnsObjectsAsFaults = false
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ private extension MyAppsViewController
|
|||||||
dataSource.cellIdentifierHandler = { _ in "UpdateCell" }
|
dataSource.cellIdentifierHandler = { _ in "UpdateCell" }
|
||||||
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
|
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let app = installedApp.storeApp else { return }
|
guard let app = installedApp.storeApp, let latestVersion = app.latestVersion else { return }
|
||||||
|
|
||||||
let cell = cell as! UpdateCollectionViewCell
|
let cell = cell as! UpdateCollectionViewCell
|
||||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||||
@@ -209,7 +209,7 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
cell.bannerView.configure(for: app)
|
cell.bannerView.configure(for: app)
|
||||||
|
|
||||||
let versionDate = Date().relativeDateString(since: app.versionDate, dateFormatter: self.dateFormatter)
|
let versionDate = Date().relativeDateString(since: latestVersion.date, dateFormatter: self.dateFormatter)
|
||||||
cell.bannerView.subtitleLabel.text = versionDate
|
cell.bannerView.subtitleLabel.text = versionDate
|
||||||
|
|
||||||
let appName: String
|
let appName: String
|
||||||
@@ -223,7 +223,7 @@ private extension MyAppsViewController
|
|||||||
appName = app.name
|
appName = app.name
|
||||||
}
|
}
|
||||||
|
|
||||||
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, app.version, versionDate)
|
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestVersion.version, versionDate)
|
||||||
|
|
||||||
cell.bannerView.button.isIndicatingActivity = false
|
cell.bannerView.button.isIndicatingActivity = false
|
||||||
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
|
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
|
||||||
@@ -461,17 +461,10 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
func updateDataSource()
|
func updateDataSource()
|
||||||
{
|
{
|
||||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
|
||||||
{
|
|
||||||
self.dataSource.predicate = nil
|
self.dataSource.predicate = nil
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.dataSource.predicate = NSPredicate(format: "%K == nil OR %K == NO OR %K == %@",
|
|
||||||
#keyPath(InstalledApp.storeApp),
|
|
||||||
#keyPath(InstalledApp.storeApp.isBeta),
|
|
||||||
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ extension UpdateCollectionViewCell
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc class UpdateCollectionViewCell: UICollectionViewCell
|
@objc final class UpdateCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
var mode: Mode = .expanded {
|
var mode: Mode = .expanded {
|
||||||
didSet {
|
didSet {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class NewsCollectionViewCell: UICollectionViewCell
|
final class NewsCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
@IBOutlet var titleLabel: UILabel!
|
@IBOutlet var titleLabel: UILabel!
|
||||||
@IBOutlet var captionLabel: UILabel!
|
@IBOutlet var captionLabel: UILabel!
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import Roxas
|
|||||||
|
|
||||||
import Nuke
|
import Nuke
|
||||||
|
|
||||||
private class AppBannerFooterView: UICollectionReusableView
|
private final class AppBannerFooterView: UICollectionReusableView
|
||||||
{
|
{
|
||||||
let bannerView = AppBannerView(frame: .zero)
|
let bannerView = AppBannerView(frame: .zero)
|
||||||
let tapGestureRecognizer = UITapGestureRecognizer(target: nil, action: nil)
|
let tapGestureRecognizer = UITapGestureRecognizer(target: nil, action: nil)
|
||||||
@@ -41,7 +41,7 @@ private class AppBannerFooterView: UICollectionReusableView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NewsViewController: UICollectionViewController
|
final class NewsViewController: UICollectionViewController
|
||||||
{
|
{
|
||||||
private lazy var dataSource = self.makeDataSource()
|
private lazy var dataSource = self.makeDataSource()
|
||||||
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
|
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
|
||||||
@@ -391,7 +391,7 @@ extension NewsViewController
|
|||||||
let progress = AppManager.shared.installationProgress(for: storeApp)
|
let progress = AppManager.shared.installationProgress(for: storeApp)
|
||||||
footerView.bannerView.button.progress = progress
|
footerView.bannerView.button.progress = progress
|
||||||
|
|
||||||
if Date() < storeApp.versionDate
|
if let versionDate = storeApp.latestVersion?.date, versionDate > Date()
|
||||||
{
|
{
|
||||||
footerView.bannerView.button.countdownDate = storeApp.versionDate
|
footerView.bannerView.button.countdownDate = storeApp.versionDate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
import Roxas
|
import Roxas
|
||||||
import Network
|
import Network
|
||||||
|
|
||||||
@@ -34,22 +35,22 @@ enum AuthenticationError: LocalizedError
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc(AuthenticationOperation)
|
@objc(AuthenticationOperation)
|
||||||
class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
|
final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
|
||||||
{
|
{
|
||||||
let context: AuthenticatedOperationContext
|
let context: AuthenticatedOperationContext
|
||||||
|
|
||||||
private weak var presentingViewController: UIViewController?
|
private weak var presentingViewController: UIViewController?
|
||||||
|
|
||||||
private lazy var navigationController: UINavigationController = {
|
// private lazy var navigationController: UINavigationController = {
|
||||||
let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
|
// let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
|
||||||
if #available(iOS 13.0, *)
|
// if #available(iOS 13.0, *)
|
||||||
{
|
// {
|
||||||
navigationController.isModalInPresentation = true
|
// navigationController.isModalInPresentation = true
|
||||||
}
|
// }
|
||||||
return navigationController
|
// return navigationController
|
||||||
}()
|
// }()
|
||||||
|
//
|
||||||
private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
|
// private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
|
||||||
|
|
||||||
private var appleIDEmailAddress: String?
|
private var appleIDEmailAddress: String?
|
||||||
private var appleIDPassword: String?
|
private var appleIDPassword: String?
|
||||||
@@ -266,7 +267,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
|
|||||||
super.finish(result)
|
super.finish(result)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.navigationController.dismiss(animated: true, completion: nil)
|
// self.navigationController.dismiss(animated: true, completion: nil)
|
||||||
|
self.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,7 +278,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
|
|||||||
super.finish(result)
|
super.finish(result)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.navigationController.dismiss(animated: true, completion: nil)
|
// self.navigationController.dismiss(animated: true, completion: nil)
|
||||||
|
self.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,25 +290,33 @@ private extension AuthenticationOperation
|
|||||||
{
|
{
|
||||||
func present(_ viewController: UIViewController) -> Bool
|
func present(_ viewController: UIViewController) -> Bool
|
||||||
{
|
{
|
||||||
guard let presentingViewController = self.presentingViewController else { return false }
|
UIApplication.shared.keyWindow?.rootViewController?.present(viewController, animated: true)
|
||||||
|
// guard let presentingViewController = self.presentingViewController else { return false }
|
||||||
self.navigationController.view.tintColor = .white
|
//
|
||||||
|
// self.navigationController.view.tintColor = .white
|
||||||
if self.navigationController.viewControllers.isEmpty
|
//
|
||||||
{
|
// if self.navigationController.viewControllers.isEmpty
|
||||||
guard presentingViewController.presentedViewController == nil else { return false }
|
// {
|
||||||
|
// guard presentingViewController.presentedViewController == nil else { return false }
|
||||||
self.navigationController.setViewControllers([viewController], animated: false)
|
//
|
||||||
presentingViewController.present(self.navigationController, animated: true, completion: nil)
|
// self.navigationController.setViewControllers([viewController], animated: false)
|
||||||
}
|
// presentingViewController.present(self.navigationController, animated: true, completion: nil)
|
||||||
else
|
// }
|
||||||
{
|
// else
|
||||||
viewController.navigationItem.leftBarButtonItem = nil
|
// {
|
||||||
self.navigationController.pushViewController(viewController, animated: true)
|
// viewController.navigationItem.leftBarButtonItem = nil
|
||||||
}
|
// self.navigationController.pushViewController(viewController, animated: true)
|
||||||
|
// }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dismiss() {
|
||||||
|
if let presentingViewController {
|
||||||
|
presentingViewController.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
// UIApplication.shared.keyWindow?.rootViewController?.presentedViewController?.dismiss(animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AuthenticationOperation
|
private extension AuthenticationOperation
|
||||||
@@ -315,29 +326,29 @@ private extension AuthenticationOperation
|
|||||||
func authenticate()
|
func authenticate()
|
||||||
{
|
{
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
|
let viewController = UIHostingController(rootView: NavigationView {
|
||||||
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
|
ConnectAppleIDView { appleID, password, completionHandler in
|
||||||
self.authenticate(appleID: appleID, password: password) { (result) in
|
self.authenticate(appleID: appleID, password: password) { (result) in
|
||||||
completionHandler(result)
|
completionHandler(result)
|
||||||
|
}
|
||||||
|
} completionHandler: { result in
|
||||||
|
if let (account, session, password) = result
|
||||||
|
{
|
||||||
|
// We presented the Auth UI and the user signed in.
|
||||||
|
// In this case, we'll assume we should show the instructions again.
|
||||||
|
self.shouldShowInstructions = true
|
||||||
|
|
||||||
|
self.appleIDPassword = password
|
||||||
|
completionHandler(.success((account, session)))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completionHandler(.failure(OperationError.cancelled))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
authenticationViewController.completionHandler = { (result) in
|
|
||||||
if let (account, session, password) = result
|
|
||||||
{
|
|
||||||
// We presented the Auth UI and the user signed in.
|
|
||||||
// In this case, we'll assume we should show the instructions again.
|
|
||||||
self.shouldShowInstructions = true
|
|
||||||
|
|
||||||
self.appleIDPassword = password
|
|
||||||
completionHandler(.success((account, session)))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
completionHandler(.failure(OperationError.cancelled))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.present(authenticationViewController)
|
if !self.present(viewController)
|
||||||
{
|
{
|
||||||
completionHandler(.failure(OperationError.notAuthenticated))
|
completionHandler(.failure(OperationError.notAuthenticated))
|
||||||
}
|
}
|
||||||
@@ -379,8 +390,8 @@ private extension AuthenticationOperation
|
|||||||
case .success(let anisetteData):
|
case .success(let anisetteData):
|
||||||
let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
|
let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
|
||||||
|
|
||||||
if let presentingViewController = self.presentingViewController
|
// if let presentingViewController = self.presentingViewController
|
||||||
{
|
// {
|
||||||
verificationHandler = { (completionHandler) in
|
verificationHandler = { (completionHandler) in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
|
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
|
||||||
@@ -406,22 +417,22 @@ private extension AuthenticationOperation
|
|||||||
completionHandler(nil)
|
completionHandler(nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
if self.navigationController.presentingViewController != nil
|
// if self.navigationController.presentingViewController != nil
|
||||||
{
|
// {
|
||||||
self.navigationController.present(alertController, animated: true, completion: nil)
|
// self.navigationController.present(alertController, animated: true, completion: nil)
|
||||||
}
|
// }
|
||||||
else
|
// else
|
||||||
{
|
// {
|
||||||
presentingViewController.present(alertController, animated: true, completion: nil)
|
// presentingViewController.present(alertController, animated: true, completion: nil)
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// }
|
||||||
else
|
// else
|
||||||
{
|
// {
|
||||||
// No view controller to present security code alert, so don't provide verificationHandler.
|
// // No view controller to present security code alert, so don't provide verificationHandler.
|
||||||
verificationHandler = nil
|
// verificationHandler = nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
|
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
|
||||||
verificationHandler: verificationHandler) { (account, session, error) in
|
verificationHandler: verificationHandler) { (account, session, error) in
|
||||||
@@ -452,15 +463,15 @@ private extension AuthenticationOperation
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
|
// let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
|
||||||
|
//
|
||||||
selectTeamViewController.teams = teams
|
// selectTeamViewController.teams = teams
|
||||||
selectTeamViewController.completionHandler = completionHandler
|
// selectTeamViewController.completionHandler = completionHandler
|
||||||
|
//
|
||||||
if !self.present(selectTeamViewController)
|
// if !self.present(selectTeamViewController)
|
||||||
{
|
// {
|
||||||
return completionHandler(.failure(AuthenticationError.noTeam))
|
// return completionHandler(.failure(AuthenticationError.noTeam))
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -642,20 +653,21 @@ private extension AuthenticationOperation
|
|||||||
|
|
||||||
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
|
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
|
||||||
{
|
{
|
||||||
guard self.shouldShowInstructions else { return completionHandler(false) }
|
return completionHandler(false)
|
||||||
|
// guard self.shouldShowInstructions else { return completionHandler(false) }
|
||||||
DispatchQueue.main.async {
|
//
|
||||||
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
|
// DispatchQueue.main.async {
|
||||||
instructionsViewController.showsBottomButton = true
|
// let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
|
||||||
instructionsViewController.completionHandler = {
|
// instructionsViewController.showsBottomButton = true
|
||||||
completionHandler(true)
|
// instructionsViewController.completionHandler = {
|
||||||
}
|
// completionHandler(true)
|
||||||
|
// }
|
||||||
if !self.present(instructionsViewController)
|
//
|
||||||
{
|
// if !self.present(instructionsViewController)
|
||||||
completionHandler(false)
|
// {
|
||||||
}
|
// completionHandler(false)
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void)
|
func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import CoreData
|
|||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import EmotionalDamage
|
import EmotionalDamage
|
||||||
|
import minimuxer
|
||||||
|
|
||||||
enum RefreshError: LocalizedError
|
enum RefreshError: LocalizedError
|
||||||
{
|
{
|
||||||
@@ -51,12 +52,12 @@ private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, Uns
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc(BackgroundRefreshAppsOperation)
|
@objc(BackgroundRefreshAppsOperation)
|
||||||
class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledApp, Error>]>
|
final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledApp, Error>]>
|
||||||
{
|
{
|
||||||
let installedApps: [InstalledApp]
|
let installedApps: [InstalledApp]
|
||||||
private let managedObjectContext: NSManagedObjectContext
|
private let managedObjectContext: NSManagedObjectContext
|
||||||
|
|
||||||
var presentsFinishedNotification: Bool = true
|
var presentsFinishedNotification: Bool = false
|
||||||
|
|
||||||
private let refreshIdentifier: String = UUID().uuidString
|
private let refreshIdentifier: String = UUID().uuidString
|
||||||
private var runningApplications: Set<String> = []
|
private var runningApplications: Set<String> = []
|
||||||
@@ -97,6 +98,14 @@ class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledA
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||||
|
target_minimuxer_address()
|
||||||
|
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||||
|
do {
|
||||||
|
try minimuxer.start(try String(contentsOf: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")), documentsDirectory)
|
||||||
|
} catch {
|
||||||
|
self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
start_auto_mounter(documentsDirectory)
|
||||||
|
|
||||||
self.managedObjectContext.perform {
|
self.managedObjectContext.perform {
|
||||||
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
|
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
|
||||||
@@ -189,12 +198,12 @@ private extension BackgroundRefreshAppsOperation
|
|||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
var shouldPresentAlert = true
|
var shouldPresentAlert = false
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let results = try result.get()
|
let results = try result.get()
|
||||||
shouldPresentAlert = !results.isEmpty
|
shouldPresentAlert = false
|
||||||
|
|
||||||
for (_, result) in results
|
for (_, result) in results
|
||||||
{
|
{
|
||||||
@@ -216,7 +225,7 @@ private extension BackgroundRefreshAppsOperation
|
|||||||
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
|
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
|
||||||
content.body = error.localizedDescription
|
content.body = error.localizedDescription
|
||||||
|
|
||||||
shouldPresentAlert = true
|
shouldPresentAlert = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldPresentAlert
|
if shouldPresentAlert
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import Roxas
|
|||||||
import minimuxer
|
import minimuxer
|
||||||
|
|
||||||
@objc(DeactivateAppOperation)
|
@objc(DeactivateAppOperation)
|
||||||
class DeactivateAppOperation: ResultOperation<InstalledApp>
|
final class DeactivateAppOperation: ResultOperation<InstalledApp>
|
||||||
{
|
{
|
||||||
let app: InstalledApp
|
let app: InstalledApp
|
||||||
let context: OperationContext
|
let context: OperationContext
|
||||||
@@ -44,14 +44,9 @@ class DeactivateAppOperation: ResultOperation<InstalledApp>
|
|||||||
|
|
||||||
for profile in allIdentifiers {
|
for profile in allIdentifiers {
|
||||||
do {
|
do {
|
||||||
let res = try remove_provisioning_profile(id: profile)
|
try remove_provisioning_profile(profile)
|
||||||
if case Uhoh.Bad(let code) = res {
|
|
||||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
|
||||||
}
|
|
||||||
} catch Uhoh.Bad(let code) {
|
|
||||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
|
||||||
} catch {
|
} catch {
|
||||||
self.finish(.failure(ALTServerError(.unknownResponse)))
|
return self.finish(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ private extension DownloadAppOperation
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc(DownloadAppOperation)
|
@objc(DownloadAppOperation)
|
||||||
class DownloadAppOperation: ResultOperation<ALTApplication>
|
final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||||
{
|
{
|
||||||
let app: AppProtocol
|
let app: AppProtocol
|
||||||
let context: AppOperationContext
|
let context: AppOperationContext
|
||||||
|
|
||||||
private let bundleIdentifier: String
|
private let bundleIdentifier: String
|
||||||
private let sourceURL: URL
|
private var sourceURL: URL?
|
||||||
private let destinationURL: URL
|
private let destinationURL: URL
|
||||||
|
|
||||||
private let session = URLSession(configuration: .default)
|
private let session = URLSession(configuration: .default)
|
||||||
@@ -69,7 +69,9 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
|
|
||||||
print("Downloading App:", self.bundleIdentifier)
|
print("Downloading App:", self.bundleIdentifier)
|
||||||
|
|
||||||
self.downloadApp(from: self.sourceURL) { result in
|
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) }
|
||||||
|
|
||||||
|
self.downloadApp(from: sourceURL) { result in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let application = try result.get()
|
let application = try result.get()
|
||||||
@@ -165,7 +167,7 @@ private extension DownloadAppOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.sourceURL.isFileURL
|
if sourceURL.isFileURL
|
||||||
{
|
{
|
||||||
finishOperation(.success(sourceURL))
|
finishOperation(.success(sourceURL))
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ protocol EnableJITContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
||||||
{
|
{
|
||||||
let context: Context
|
let context: Context
|
||||||
|
|
||||||
@@ -45,23 +45,13 @@ class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
|||||||
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
|
|
||||||
installedApp.managedObjectContext?.perform {
|
installedApp.managedObjectContext?.perform {
|
||||||
let v = minimuxer_to_operation(code: 1)
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
var x = try debug_app(app_id: installedApp.resignedBundleIdentifier)
|
try debug_app(installedApp.resignedBundleIdentifier)
|
||||||
switch x {
|
|
||||||
case .Good:
|
|
||||||
self.finish(.success(()))
|
|
||||||
case .Bad(let code):
|
|
||||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
|
||||||
}
|
|
||||||
} catch Uhoh.Bad(let code) {
|
|
||||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
|
||||||
} catch {
|
} catch {
|
||||||
self.finish(.failure(OperationError.unknown))
|
return self.finish(.failure(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.finish(.success(()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,28 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CommonCrypto
|
||||||
|
import Starscream
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
@objc(FetchAnisetteDataOperation)
|
@objc(FetchAnisetteDataOperation)
|
||||||
class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
|
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSocketDelegate
|
||||||
{
|
{
|
||||||
let context: OperationContext
|
let context: OperationContext
|
||||||
|
var socket: WebSocket!
|
||||||
|
|
||||||
|
var url: URL?
|
||||||
|
var startProvisioningURL: URL?
|
||||||
|
var endProvisioningURL: URL?
|
||||||
|
|
||||||
|
var clientInfo: String?
|
||||||
|
var userAgent: String?
|
||||||
|
|
||||||
|
var mdLu: String?
|
||||||
|
var deviceId: String?
|
||||||
|
|
||||||
init(context: OperationContext)
|
init(context: OperationContext)
|
||||||
{
|
{
|
||||||
@@ -32,31 +45,412 @@ class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = AnisetteManager.currentURL
|
self.url = AnisetteManager.currentURL
|
||||||
DLOG("Anisette URL: %@", url.absoluteString)
|
print("Anisette URL: \(self.url!.absoluteString)")
|
||||||
|
|
||||||
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
if let identifier = Keychain.shared.identifier,
|
||||||
guard let data = data, error == nil else { return }
|
let adiPb = Keychain.shared.adiPb {
|
||||||
|
fetchAnisetteV3(identifier, adiPb)
|
||||||
do {
|
} else {
|
||||||
// make sure this JSON is in the format we expect
|
provision()
|
||||||
// convert data to json
|
}
|
||||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
}
|
||||||
// try to read out a dictionary
|
|
||||||
//for some reason serial number isn't needed but it doesn't work unless it has a value
|
// MARK: - COMMON
|
||||||
let formattedJSON: [String: String] = ["machineID": json["X-Apple-I-MD-M"]!, "oneTimePassword": json["X-Apple-I-MD"]!, "localUserID": json["X-Apple-I-MD-LU"]!, "routingInfo": json["X-Apple-I-MD-RINFO"]!, "deviceUniqueIdentifier": json["X-Mme-Device-Id"]!, "deviceDescription": json["X-MMe-Client-Info"]!, "date": json["X-Apple-I-Client-Time"]!, "locale": json["X-Apple-Locale"]!, "timeZone": json["X-Apple-I-TimeZone"]!, "deviceSerialNumber": "1"]
|
|
||||||
|
func extractAnisetteData(_ data: Data, _ response: HTTPURLResponse?, v3: Bool) throws {
|
||||||
if let anisette = ALTAnisetteData(json: formattedJSON) {
|
// make sure this JSON is in the format we expect
|
||||||
self.finish(.success(anisette))
|
// convert data to json
|
||||||
}
|
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
||||||
|
if v3 {
|
||||||
|
if json["result"] == "GetHeadersError" {
|
||||||
|
let message = json["message"]
|
||||||
|
print("Error getting V3 headers: \(message ?? "no message")")
|
||||||
|
if let message = message,
|
||||||
|
message.contains("-45061") {
|
||||||
|
print("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
|
||||||
|
Keychain.shared.adiPb = nil
|
||||||
|
return provision()
|
||||||
|
} else { throw OperationError.anisetteV3Error(message: message ?? "Unknown error") }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to read out a dictionary
|
||||||
|
// for some reason serial number isn't needed but it doesn't work unless it has a value
|
||||||
|
var formattedJSON: [String: String] = ["deviceSerialNumber": "0"]
|
||||||
|
if let machineID = json["X-Apple-I-MD-M"] { formattedJSON["machineID"] = machineID }
|
||||||
|
if let oneTimePassword = json["X-Apple-I-MD"] { formattedJSON["oneTimePassword"] = oneTimePassword }
|
||||||
|
if let routingInfo = json["X-Apple-I-MD-RINFO"] { formattedJSON["routingInfo"] = routingInfo }
|
||||||
|
|
||||||
|
if v3 {
|
||||||
|
formattedJSON["deviceDescription"] = self.clientInfo!
|
||||||
|
formattedJSON["localUserID"] = self.mdLu!
|
||||||
|
formattedJSON["deviceUniqueIdentifier"] = self.deviceId!
|
||||||
|
|
||||||
|
// Generate date stuff on client
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
formatter.calendar = Calendar(identifier: .gregorian)
|
||||||
|
formatter.timeZone = TimeZone.current
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||||
|
let dateString = formatter.string(from: Date())
|
||||||
|
formattedJSON["date"] = dateString
|
||||||
|
formattedJSON["locale"] = Locale.current.identifier
|
||||||
|
formattedJSON["timeZone"] = TimeZone.current.abbreviation()
|
||||||
|
} else {
|
||||||
|
if let deviceDescription = json["X-MMe-Client-Info"] { formattedJSON["deviceDescription"] = deviceDescription }
|
||||||
|
if let localUserID = json["X-Apple-I-MD-LU"] { formattedJSON["localUserID"] = localUserID }
|
||||||
|
if let deviceUniqueIdentifier = json["X-Mme-Device-Id"] { formattedJSON["deviceUniqueIdentifier"] = deviceUniqueIdentifier }
|
||||||
|
|
||||||
|
if let date = json["X-Apple-I-Client-Time"] { formattedJSON["date"] = date }
|
||||||
|
if let locale = json["X-Apple-Locale"] { formattedJSON["locale"] = locale }
|
||||||
|
if let timeZone = json["X-Apple-I-TimeZone"] { formattedJSON["timeZone"] = timeZone }
|
||||||
|
}
|
||||||
|
|
||||||
|
if let response = response,
|
||||||
|
let version = response.value(forHTTPHeaderField: "Implementation-Version") {
|
||||||
|
print("Implementation-Version: \(version)")
|
||||||
|
} else { print("No Implementation-Version header") }
|
||||||
|
|
||||||
|
print("Anisette used: \(formattedJSON)")
|
||||||
|
print("Original JSON: \(json)")
|
||||||
|
if let anisette = ALTAnisetteData(json: formattedJSON) {
|
||||||
|
print("Anisette is valid!")
|
||||||
|
self.finish(.success(anisette))
|
||||||
|
} else {
|
||||||
|
print("Anisette is invalid!!!!")
|
||||||
|
if v3 {
|
||||||
|
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not have all the required fields)")
|
||||||
|
} else {
|
||||||
|
throw OperationError.anisetteV1Error(message: "Invalid anisette (the returned data may not have all the required fields)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if v3 {
|
||||||
|
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not be in JSON)")
|
||||||
|
} else {
|
||||||
|
throw OperationError.anisetteV1Error(message: "Invalid anisette (the returned data may not be in JSON)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - V1
|
||||||
|
|
||||||
|
func handleV1() {
|
||||||
|
print("Server is V1")
|
||||||
|
|
||||||
|
if UserDefaults.shared.trustedServerURL == AnisetteManager.currentURLString {
|
||||||
|
print("Server has already been trusted, fetching anisette")
|
||||||
|
return self.fetchAnisetteV1()
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Alerting user about outdated server")
|
||||||
|
let alert = UIAlertController(title: "WARNING: Outdated anisette server", message: "We've detected you are using an older anisette server. Using this server has a higher likelihood of locking your account and causing other issues. Are you sure you want to continue?", preferredStyle: UIAlertController.Style.alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "Continue", style: UIAlertAction.Style.destructive, handler: { action in
|
||||||
|
print("Fetching anisette via V1")
|
||||||
|
UserDefaults.shared.trustedServerURL = AnisetteManager.currentURLString
|
||||||
|
self.fetchAnisetteV1()
|
||||||
|
}))
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: { action in
|
||||||
|
print("Cancelled anisette operation")
|
||||||
|
self.finish(.failure(OperationError.cancelled))
|
||||||
|
}))
|
||||||
|
|
||||||
|
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let presentingController = keyWindow?.rootViewController?.presentedViewController {
|
||||||
|
presentingController.present(alert, animated: true)
|
||||||
|
} else {
|
||||||
|
keyWindow?.rootViewController?.present(alert, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAnisetteV1() {
|
||||||
|
print("Fetching anisette V1")
|
||||||
|
URLSession.shared.dataTask(with: self.url!) { data, response, error in
|
||||||
|
do {
|
||||||
|
guard let data = data, error == nil else { throw OperationError.anisetteV1Error(message: "Unable to fetch data\(error != nil ? " (\(error!.localizedDescription))" : "")") }
|
||||||
|
|
||||||
|
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: false)
|
||||||
} catch let error as NSError {
|
} catch let error as NSError {
|
||||||
print("Failed to load: \(error.localizedDescription)")
|
print("Failed to load: \(error.localizedDescription)")
|
||||||
self.finish(.failure(error))
|
self.finish(.failure(error))
|
||||||
}
|
}
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - V3: PROVISIONING
|
||||||
|
|
||||||
|
func provision() {
|
||||||
|
fetchClientInfo {
|
||||||
|
print("Getting provisioning URLs")
|
||||||
|
var request = self.buildAppleRequest(url: URL(string: "https://gsa.apple.com/grandslam/GsService2/lookup")!)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
if let data = data,
|
||||||
|
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||||
|
let startProvisioningString = plist["urls"]?["midStartProvisioning"] as? String,
|
||||||
|
let startProvisioningURL = URL(string: startProvisioningString),
|
||||||
|
let endProvisioningString = plist["urls"]?["midFinishProvisioning"] as? String,
|
||||||
|
let endProvisioningURL = URL(string: endProvisioningString) {
|
||||||
|
self.startProvisioningURL = startProvisioningURL
|
||||||
|
self.endProvisioningURL = endProvisioningURL
|
||||||
|
print("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
|
||||||
|
print("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
|
||||||
|
print("Starting a provisioning session")
|
||||||
|
self.startProvisioningSession()
|
||||||
|
} else {
|
||||||
|
print("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||||
|
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid URLs. Please try again later", message: nil)))
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startProvisioningSession() {
|
||||||
|
let provisioningSessionURL = self.url!.appendingPathComponent("v3").appendingPathComponent("provisioning_session")
|
||||||
|
var wsRequest = URLRequest(url: provisioningSessionURL)
|
||||||
|
wsRequest.timeoutInterval = 5
|
||||||
|
self.socket = WebSocket(request: wsRequest)
|
||||||
|
self.socket.delegate = self
|
||||||
|
self.socket.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func didReceive(event: WebSocketEvent, client: WebSocket) {
|
||||||
|
switch event {
|
||||||
|
case .text(let string):
|
||||||
|
do {
|
||||||
|
if let json = try JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: []) as? [String: Any] {
|
||||||
|
guard let result = json["result"] as? String else {
|
||||||
|
print("The server didn't give us a result")
|
||||||
|
client.disconnect(closeCode: 0)
|
||||||
|
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a result", message: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("Received result: \(result)")
|
||||||
|
switch result {
|
||||||
|
case "GiveIdentifier":
|
||||||
|
print("Giving identifier")
|
||||||
|
client.json(["identifier": Keychain.shared.identifier!])
|
||||||
|
|
||||||
|
case "GiveStartProvisioningData":
|
||||||
|
print("Getting start provisioning data")
|
||||||
|
let body = [
|
||||||
|
"Header": [String: Any](),
|
||||||
|
"Request": [String: Any](),
|
||||||
|
]
|
||||||
|
var request = self.buildAppleRequest(url: self.startProvisioningURL!)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.httpBody = try! PropertyListSerialization.data(fromPropertyList: body, format: .xml, options: 0)
|
||||||
|
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
if let data = data,
|
||||||
|
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||||
|
let spim = plist["Response"]?["spim"] as? String {
|
||||||
|
print("Giving start provisioning data")
|
||||||
|
client.json(["spim": spim])
|
||||||
|
} else {
|
||||||
|
print("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||||
|
client.disconnect(closeCode: 0)
|
||||||
|
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid start provisioning data. Please try again later", message: nil)))
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
|
||||||
|
case "GiveEndProvisioningData":
|
||||||
|
print("Getting end provisioning data")
|
||||||
|
guard let cpim = json["cpim"] as? String else {
|
||||||
|
print("The server didn't give us a cpim")
|
||||||
|
client.disconnect(closeCode: 0)
|
||||||
|
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a cpim", message: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let body = [
|
||||||
|
"Header": [String: Any](),
|
||||||
|
"Request": [
|
||||||
|
"cpim": cpim,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
var request = self.buildAppleRequest(url: self.endProvisioningURL!)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.httpBody = try! PropertyListSerialization.data(fromPropertyList: body, format: .xml, options: 0)
|
||||||
|
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
if let data = data,
|
||||||
|
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||||
|
let ptm = plist["Response"]?["ptm"] as? String,
|
||||||
|
let tk = plist["Response"]?["tk"] as? String {
|
||||||
|
print("Giving end provisioning data")
|
||||||
|
client.json(["ptm": ptm, "tk": tk])
|
||||||
|
} else {
|
||||||
|
print("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||||
|
client.disconnect(closeCode: 0)
|
||||||
|
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid end provisioning data. Please try again later", message: nil)))
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
|
||||||
|
case "ProvisioningSuccess":
|
||||||
|
print("Provisioning succeeded!")
|
||||||
|
client.disconnect(closeCode: 0)
|
||||||
|
guard let adiPb = json["adi_pb"] as? String else {
|
||||||
|
print("The server didn't give us an adi.pb file")
|
||||||
|
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us an adi.pb file", message: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Keychain.shared.adiPb = adiPb
|
||||||
|
self.fetchAnisetteV3(Keychain.shared.identifier!, Keychain.shared.adiPb!)
|
||||||
|
|
||||||
|
default:
|
||||||
|
if result.contains("Error") || result.contains("Invalid") || result == "ClosingPerRequest" || result == "Timeout" || result == "TextOnly" {
|
||||||
|
print("Failing because of \(result)")
|
||||||
|
self.finish(.failure(OperationError.provisioningError(result: result, message: json["message"] as? String)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch let error as NSError {
|
||||||
|
print("Failed to handle text: \(error.localizedDescription)")
|
||||||
|
self.finish(.failure(OperationError.provisioningError(result: error.localizedDescription, message: nil)))
|
||||||
|
}
|
||||||
|
|
||||||
|
case .connected:
|
||||||
|
print("Connected")
|
||||||
|
|
||||||
|
case .disconnected(let string, let code):
|
||||||
|
print("Disconnected: \(code); \(string)")
|
||||||
|
|
||||||
|
case .error(let error):
|
||||||
|
print("Got error: \(String(describing: error))")
|
||||||
|
|
||||||
|
default:
|
||||||
|
print("Unknown event: \(event)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAppleRequest(url: URL) -> URLRequest {
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.setValue(self.clientInfo!, forHTTPHeaderField: "X-Mme-Client-Info")
|
||||||
|
request.setValue(self.userAgent!, forHTTPHeaderField: "User-Agent")
|
||||||
|
request.setValue("text/x-xml-plist", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.setValue("*/*", forHTTPHeaderField: "Accept")
|
||||||
|
|
||||||
|
request.setValue(self.mdLu!, forHTTPHeaderField: "X-Apple-I-MD-LU")
|
||||||
|
request.setValue(self.deviceId!, forHTTPHeaderField: "X-Mme-Device-Id")
|
||||||
|
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
formatter.calendar = Calendar(identifier: .gregorian)
|
||||||
|
formatter.timeZone = TimeZone(identifier: "UTC")
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||||
|
let dateString = formatter.string(from: Date())
|
||||||
|
request.setValue(dateString, forHTTPHeaderField: "X-Apple-I-Client-Time")
|
||||||
|
request.setValue(Locale.current.identifier, forHTTPHeaderField: "X-Apple-Locale")
|
||||||
|
request.setValue(TimeZone.current.abbreviation(), forHTTPHeaderField: "X-Apple-I-TimeZone")
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - V3: FETCHING
|
||||||
|
|
||||||
|
func fetchClientInfo(_ callback: @escaping () -> Void) {
|
||||||
|
if self.clientInfo != nil &&
|
||||||
|
self.userAgent != nil &&
|
||||||
|
self.mdLu != nil &&
|
||||||
|
self.deviceId != nil &&
|
||||||
|
Keychain.shared.identifier != nil {
|
||||||
|
print("Skipping client_info fetch since all the properties we need aren't nil")
|
||||||
|
return callback()
|
||||||
|
}
|
||||||
|
print("Trying to get client_info")
|
||||||
|
let clientInfoURL = self.url!.appendingPathComponent("v3").appendingPathComponent("client_info")
|
||||||
|
URLSession.shared.dataTask(with: clientInfoURL) { data, response, error in
|
||||||
|
do {
|
||||||
|
guard let data = data, error == nil else {
|
||||||
|
return self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The server may be down\(error != nil ? " (\(error!.localizedDescription))" : "")")))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
||||||
|
if let clientInfo = json["client_info"] {
|
||||||
|
print("Server is V3")
|
||||||
|
|
||||||
|
self.clientInfo = clientInfo
|
||||||
|
self.userAgent = json["user_agent"]!
|
||||||
|
print("Client-Info: \(self.clientInfo!)")
|
||||||
|
print("User-Agent: \(self.userAgent!)")
|
||||||
|
|
||||||
|
if Keychain.shared.identifier == nil {
|
||||||
|
print("Generating identifier")
|
||||||
|
var bytes = [Int8](repeating: 0, count: 16)
|
||||||
|
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
||||||
|
|
||||||
|
if status != errSecSuccess {
|
||||||
|
print("ERROR GENERATING IDENTIFIER!!! \(status)")
|
||||||
|
return self.finish(.failure(OperationError.provisioningError(result: "Couldn't generate identifier", message: nil)))
|
||||||
|
}
|
||||||
|
|
||||||
|
Keychain.shared.identifier = Data(bytes: &bytes, count: bytes.count).base64EncodedString()
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded = Data(base64Encoded: Keychain.shared.identifier!)!
|
||||||
|
self.mdLu = decoded.sha256().hexEncodedString()
|
||||||
|
print("X-Apple-I-MD-LU: \(self.mdLu!)")
|
||||||
|
let uuid: UUID = decoded.object()
|
||||||
|
self.deviceId = uuid.uuidString.uppercased()
|
||||||
|
print("X-Mme-Device-Id: \(self.deviceId!)")
|
||||||
|
|
||||||
|
callback()
|
||||||
|
} else { self.handleV1() }
|
||||||
|
} else { self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The returned data may not be in JSON"))) }
|
||||||
|
} catch let error as NSError {
|
||||||
|
print("Failed to load: \(error.localizedDescription)")
|
||||||
|
self.handleV1()
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
|
||||||
|
fetchClientInfo {
|
||||||
|
print("Fetching anisette V3")
|
||||||
|
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.httpBody = try! JSONSerialization.data(withJSONObject: [
|
||||||
|
"identifier": identifier,
|
||||||
|
"adi_pb": adiPb
|
||||||
|
], options: [])
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
do {
|
||||||
|
guard let data = data, error == nil else { throw OperationError.anisetteV3Error(message: "Couldn't fetch anisette") }
|
||||||
|
|
||||||
|
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: true)
|
||||||
|
} catch let error as NSError {
|
||||||
|
print("Failed to load: \(error.localizedDescription)")
|
||||||
|
self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
task.resume()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension WebSocket {
|
||||||
|
func json(_ dictionary: [String: String]) {
|
||||||
|
let data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
|
||||||
|
self.write(string: String(data: data, encoding: .utf8)!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Data {
|
||||||
|
// https://stackoverflow.com/a/25391020
|
||||||
|
func sha256() -> Data {
|
||||||
|
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||||
|
self.withUnsafeBytes {
|
||||||
|
_ = CC_SHA256($0.baseAddress, CC_LONG(self.count), &hash)
|
||||||
|
}
|
||||||
|
return Data(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/40089462
|
||||||
|
func hexEncodedString() -> String {
|
||||||
|
return self.map { String(format: "%02hhX", $0) }.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/59127761
|
||||||
|
func object<T>() -> T { self.withUnsafeBytes { $0.load(as: T.self) } }
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import AltSign
|
|||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
@objc(FetchAppIDsOperation)
|
@objc(FetchAppIDsOperation)
|
||||||
class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
|
final class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
|
||||||
{
|
{
|
||||||
let context: AuthenticatedOperationContext
|
let context: AuthenticatedOperationContext
|
||||||
let managedObjectContext: NSManagedObjectContext
|
let managedObjectContext: NSManagedObjectContext
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import AltSign
|
|||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
@objc(FetchProvisioningProfilesOperation)
|
@objc(FetchProvisioningProfilesOperation)
|
||||||
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
|
final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
|
||||||
{
|
{
|
||||||
let context: AppOperationContext
|
let context: AppOperationContext
|
||||||
|
|
||||||
@@ -131,19 +131,19 @@ extension FetchProvisioningProfilesOperation
|
|||||||
// or if installedApp.team is nil but resignedBundleIdentifier contains the team's identifier.
|
// or if installedApp.team is nil but resignedBundleIdentifier contains the team's identifier.
|
||||||
let teamsMatch = installedApp.team?.identifier == team.identifier || (installedApp.team == nil && installedApp.resignedBundleIdentifier.contains(team.identifier))
|
let teamsMatch = installedApp.team?.identifier == team.identifier || (installedApp.team == nil && installedApp.resignedBundleIdentifier.contains(team.identifier))
|
||||||
|
|
||||||
#if DEBUG
|
// #if DEBUG
|
||||||
|
//
|
||||||
if app.isAltStoreApp
|
// if app.isAltStoreApp
|
||||||
{
|
// {
|
||||||
// Use legacy bundle ID format for AltStore.
|
// // Use legacy bundle ID format for AltStore.
|
||||||
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
||||||
}
|
// }
|
||||||
else
|
// else
|
||||||
{
|
// {
|
||||||
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
#else
|
// #else
|
||||||
|
|
||||||
if teamsMatch
|
if teamsMatch
|
||||||
{
|
{
|
||||||
@@ -157,7 +157,7 @@ extension FetchProvisioningProfilesOperation
|
|||||||
preferredBundleID = nil
|
preferredBundleID = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
// #endif
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -268,8 +268,17 @@ extension FetchProvisioningProfilesOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//App ID name must be ascii. If the name is not ascii, using bundleID instead
|
||||||
|
let appIDName: String
|
||||||
|
if !name.allSatisfy({ $0.isASCII }) {
|
||||||
|
//Contains non ASCII (Such as Chinese/Japanese...), using bundleID
|
||||||
|
appIDName = bundleIdentifier
|
||||||
|
}else {
|
||||||
|
//ASCII text, keep going as usual
|
||||||
|
appIDName = name
|
||||||
|
}
|
||||||
|
|
||||||
ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
|
ALTAppleAPI.shared.addAppID(withName: appIDName, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
@@ -384,19 +393,39 @@ extension FetchProvisioningProfilesOperation
|
|||||||
|
|
||||||
if app.isAltStoreApp
|
if app.isAltStoreApp
|
||||||
{
|
{
|
||||||
|
print("Application groups before modifying for SideStore: \(applicationGroups)")
|
||||||
|
|
||||||
|
// Remove app groups that contain AltStore since they can be problematic (cause SideStore to expire early)
|
||||||
|
for (index, group) in applicationGroups.enumerated() {
|
||||||
|
if group.contains("AltStore") {
|
||||||
|
print("Removing application group: \(group)")
|
||||||
|
applicationGroups.remove(at: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we add .AltWidget for the widget
|
||||||
|
var altStoreAppGroupID = Bundle.baseAltStoreAppGroupID
|
||||||
|
for (_, group) in applicationGroups.enumerated() {
|
||||||
|
if group.contains("AltWidget") {
|
||||||
|
altStoreAppGroupID += ".AltWidget"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Potentially updating app groups for this specific AltStore.
|
// Potentially updating app groups for this specific AltStore.
|
||||||
// Find the (unique) AltStore app group, then replace it
|
// Find the (unique) AltStore app group, then replace it
|
||||||
// with the correct "base" app group ID.
|
// with the correct "base" app group ID.
|
||||||
// Otherwise, we may append a duplicate team identifier to the end.
|
// Otherwise, we may append a duplicate team identifier to the end.
|
||||||
if let index = applicationGroups.firstIndex(where: { $0.contains(Bundle.baseAltStoreAppGroupID) })
|
if let index = applicationGroups.firstIndex(where: { $0.contains(Bundle.baseAltStoreAppGroupID) })
|
||||||
{
|
{
|
||||||
applicationGroups[index] = Bundle.baseAltStoreAppGroupID
|
applicationGroups[index] = altStoreAppGroupID
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
applicationGroups.append(Bundle.baseAltStoreAppGroupID)
|
applicationGroups.append(altStoreAppGroupID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
print("Application groups: \(applicationGroups)")
|
||||||
|
|
||||||
// Dispatch onto global queue to prevent appGroupsLock deadlock.
|
// Dispatch onto global queue to prevent appGroupsLock deadlock.
|
||||||
DispatchQueue.global().async {
|
DispatchQueue.global().async {
|
||||||
@@ -478,10 +507,13 @@ extension FetchProvisioningProfilesOperation
|
|||||||
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
|
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
|
||||||
switch Result(success, error)
|
switch Result(success, error)
|
||||||
{
|
{
|
||||||
case .failure(let error): completionHandler(.failure(error))
|
case .failure:
|
||||||
case .success:
|
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
|
||||||
|
// So instead, we just return the fetched profile from above.
|
||||||
|
completionHandler(.success(profile))
|
||||||
|
|
||||||
// Fetch new provisiong profile
|
case .success:
|
||||||
|
// Fetch new provisioning profile
|
||||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
|
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
|
||||||
completionHandler(Result(profile, error))
|
completionHandler(Result(profile, error))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import AltStoreCore
|
|||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
@objc(FetchSourceOperation)
|
@objc(FetchSourceOperation)
|
||||||
class FetchSourceOperation: ResultOperation<Source>
|
final class FetchSourceOperation: ResultOperation<Source>
|
||||||
{
|
{
|
||||||
let sourceURL: URL
|
let sourceURL: URL
|
||||||
let managedObjectContext: NSManagedObjectContext
|
let managedObjectContext: NSManagedObjectContext
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ extension FetchTrustedSourcesOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FetchTrustedSourcesOperation: ResultOperation<[FetchTrustedSourcesOperation.TrustedSource]>
|
final class FetchTrustedSourcesOperation: ResultOperation<[FetchTrustedSourcesOperation.TrustedSource]>
|
||||||
{
|
{
|
||||||
override func main()
|
override func main()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import Network
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
import Roxas
|
import Roxas
|
||||||
|
import minimuxer
|
||||||
|
|
||||||
@objc(InstallAppOperation)
|
@objc(InstallAppOperation)
|
||||||
class InstallAppOperation: ResultOperation<InstalledApp>
|
final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||||
{
|
{
|
||||||
let context: InstallAppOperationContext
|
let context: InstallAppOperationContext
|
||||||
|
|
||||||
@@ -86,6 +87,11 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
let resignedBundleID = appExtension.bundleIdentifier
|
let resignedBundleID = appExtension.bundleIdentifier
|
||||||
let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
|
let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
|
||||||
|
|
||||||
|
print("`parentBundleID`: \(parentBundleID)")
|
||||||
|
print("`resignedParentBundleID`: \(resignedParentBundleID)")
|
||||||
|
print("`resignedBundleID`: \(resignedBundleID)")
|
||||||
|
print("`originalBundleID`: \(originalBundleID)")
|
||||||
|
|
||||||
let installedExtension: InstalledExtension
|
let installedExtension: InstalledExtension
|
||||||
|
|
||||||
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
|
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
|
||||||
@@ -143,17 +149,72 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let ns_bundle = NSString(string: installedApp.bundleIdentifier)
|
var installing = true
|
||||||
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
|
if installedApp.storeApp?.bundleIdentifier == Bundle.Info.appbundleIdentifier {
|
||||||
|
// Reinstalling ourself will hang until we leave the app, so we need to exit it without force closing
|
||||||
let res = minimuxer_install_ipa(ns_bundle_ptr)
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||||
if res == 0 {
|
if UIApplication.shared.applicationState != .active {
|
||||||
installedApp.refreshedDate = Date()
|
print("We are not in the foreground, let's not do anything")
|
||||||
self.finish(.success(installedApp))
|
return
|
||||||
|
}
|
||||||
} else {
|
if !installing {
|
||||||
self.finish(.failure(minimuxer_to_operation(code: res)))
|
print("Installing finished")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("We are still installing after 3 seconds")
|
||||||
|
|
||||||
|
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||||
|
switch (settings.authorizationStatus) {
|
||||||
|
case .authorized, .ephemeral, .provisional:
|
||||||
|
print("Notifications are enabled")
|
||||||
|
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "Refreshing..."
|
||||||
|
content.body = "To finish refreshing, SideStore must be moved to the background, which it does by opening Safari. Please reopen SideStore after it is done refreshing!"
|
||||||
|
let notification = UNNotificationRequest(identifier: Bundle.Info.appbundleIdentifier + ".FinishRefreshNotification", content: content, trigger: UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false))
|
||||||
|
UNUserNotificationCenter.current().add(notification)
|
||||||
|
|
||||||
|
DispatchQueue.main.async { UIApplication.shared.open(URL(string: "x-web-search://")!) }
|
||||||
|
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
print("Notifications are not enabled")
|
||||||
|
|
||||||
|
let alert = UIAlertController(title: "Finish Refresh", message: "To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen or open Safari by pressing Continue. Please reopen SideStore after doing this.", preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default, handler: { _ in
|
||||||
|
print("Opening Safari")
|
||||||
|
DispatchQueue.main.async { UIApplication.shared.open(URL(string: "x-web-search://")!) }
|
||||||
|
}))
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
|
||||||
|
if var topController = keyWindow?.rootViewController {
|
||||||
|
while let presentedViewController = topController.presentedViewController {
|
||||||
|
topController = presentedViewController
|
||||||
|
}
|
||||||
|
topController.present(alert, animated: true)
|
||||||
|
} else {
|
||||||
|
print("No key window? Let's just open Safari")
|
||||||
|
UIApplication.shared.open(URL(string: "x-web-search://")!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try install_ipa(installedApp.bundleIdentifier)
|
||||||
|
installing = false
|
||||||
|
} catch {
|
||||||
|
installing = false
|
||||||
|
return self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
installedApp.refreshedDate = Date()
|
||||||
|
self.finish(.success(installedApp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,10 +230,11 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
try FileManager.default.removeItem(at: fileURL)
|
try FileManager.default.removeItem(at: fileURL)
|
||||||
|
print("Removed refreshed IPA")
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
print("Failed to remove refreshed .ipa:", error)
|
print("Failed to remove refreshed .ipa: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class OperationContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthenticatedOperationContext: OperationContext
|
final class AuthenticatedOperationContext: OperationContext
|
||||||
{
|
{
|
||||||
var session: ALTAppleAPISession?
|
var session: ALTAppleAPISession?
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,12 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import AltSign
|
import AltSign
|
||||||
|
import minimuxer
|
||||||
|
|
||||||
enum OperationError: LocalizedError
|
enum OperationError: LocalizedError
|
||||||
{
|
{
|
||||||
|
static let domain = OperationError.unknown._domain
|
||||||
|
|
||||||
case unknown
|
case unknown
|
||||||
case unknownResult
|
case unknownResult
|
||||||
case cancelled
|
case cancelled
|
||||||
@@ -31,18 +34,9 @@ enum OperationError: LocalizedError
|
|||||||
case openAppFailed(name: String)
|
case openAppFailed(name: String)
|
||||||
case missingAppGroup
|
case missingAppGroup
|
||||||
|
|
||||||
case noDevice
|
case anisetteV1Error(message: String)
|
||||||
case createService(name: String)
|
case provisioningError(result: String, message: String?)
|
||||||
case getFromDevice(name: String)
|
case anisetteV3Error(message: String)
|
||||||
case setArgument(name: String)
|
|
||||||
case afc
|
|
||||||
case install
|
|
||||||
case uninstall
|
|
||||||
case lookupApps
|
|
||||||
case detach
|
|
||||||
case functionArguments
|
|
||||||
case profileInstall
|
|
||||||
case noConnection
|
|
||||||
|
|
||||||
var failureReason: String? {
|
var failureReason: String? {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -59,18 +53,9 @@ enum OperationError: LocalizedError
|
|||||||
case .openAppFailed(let name): return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), name)
|
case .openAppFailed(let name): return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), name)
|
||||||
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be found.", comment: "")
|
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be found.", comment: "")
|
||||||
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
|
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
|
||||||
case .noDevice: return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
|
case .anisetteV1Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: ""), message)
|
||||||
case .createService(let name): return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
|
case .provisioningError(let result, let message): return String(format: NSLocalizedString("An error occurred when provisioning: %@%@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), result, message != nil ? (" (" + message! + ")") : "")
|
||||||
case .getFromDevice(let name): return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
|
case .anisetteV3Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), message)
|
||||||
case .setArgument(let name): return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
|
|
||||||
case .afc: return NSLocalizedString("AFC was unable to manage files on the device", comment: "")
|
|
||||||
case .install: return NSLocalizedString("Unable to install the app from the staging directory", comment: "")
|
|
||||||
case .uninstall: return NSLocalizedString("Unable to uninstall the app", comment: "")
|
|
||||||
case .lookupApps: return NSLocalizedString("Unable to fetch apps from the device", comment: "")
|
|
||||||
case .detach: return NSLocalizedString("Unable to detach from the app's process", comment: "")
|
|
||||||
case .functionArguments: return NSLocalizedString("A function was passed invalid arguments", comment: "")
|
|
||||||
case .profileInstall: return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
|
||||||
case .noConnection: return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,49 +101,66 @@ enum OperationError: LocalizedError
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func minimuxer_to_operation(code: Int32) -> OperationError {
|
extension MinimuxerError: LocalizedError {
|
||||||
switch code {
|
public var failureReason: String? {
|
||||||
case -1:
|
switch self {
|
||||||
return OperationError.noDevice
|
case .NoDevice:
|
||||||
case -2:
|
return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
|
||||||
return OperationError.createService(name: "debug")
|
case .NoConnection:
|
||||||
case -3:
|
return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "")
|
||||||
return OperationError.createService(name: "instproxy")
|
case .PairingFile:
|
||||||
case -4:
|
return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use jitterbugpair to generate it", comment: "")
|
||||||
return OperationError.getFromDevice(name: "installed apps")
|
|
||||||
case -5:
|
case .CreateDebug:
|
||||||
return OperationError.getFromDevice(name: "path to the app")
|
return self.createService(name: "debug")
|
||||||
case -6:
|
case .LookupApps:
|
||||||
return OperationError.getFromDevice(name: "bundle path")
|
return self.getFromDevice(name: "installed apps")
|
||||||
case -7:
|
case .FindApp:
|
||||||
return OperationError.setArgument(name: "max packet")
|
return self.getFromDevice(name: "path to the app")
|
||||||
case -8:
|
case .BundlePath:
|
||||||
return OperationError.setArgument(name: "working directory")
|
return self.getFromDevice(name: "bundle path")
|
||||||
case -9:
|
case .MaxPacket:
|
||||||
return OperationError.setArgument(name: "argv")
|
return self.setArgument(name: "max packet")
|
||||||
case -10:
|
case .WorkingDirectory:
|
||||||
return OperationError.getFromDevice(name: "launch success")
|
return self.setArgument(name: "working directory")
|
||||||
case -11:
|
case .Argv:
|
||||||
return OperationError.detach
|
return self.setArgument(name: "argv")
|
||||||
case -12:
|
case .LaunchSuccess:
|
||||||
return OperationError.functionArguments
|
return self.getFromDevice(name: "launch success")
|
||||||
case -13:
|
case .Detach:
|
||||||
return OperationError.createService(name: "AFC")
|
return NSLocalizedString("Unable to detach from the app's process", comment: "")
|
||||||
case -14:
|
case .Attach:
|
||||||
return OperationError.afc
|
return NSLocalizedString("Unable to attach to the app's process", comment: "")
|
||||||
case -15:
|
|
||||||
return OperationError.install
|
case .CreateInstproxy:
|
||||||
case -16:
|
return self.createService(name: "instproxy")
|
||||||
return OperationError.uninstall
|
case .CreateAfc:
|
||||||
case -17:
|
return self.createService(name: "AFC")
|
||||||
return OperationError.createService(name: "misagent")
|
case .RwAfc:
|
||||||
case -18:
|
return NSLocalizedString("AFC was unable to manage files on the device", comment: "")
|
||||||
return OperationError.profileInstall
|
case .InstallApp:
|
||||||
case -19:
|
return NSLocalizedString("Unable to install the app from the staging directory", comment: "")
|
||||||
return OperationError.profileInstall
|
case .UninstallApp:
|
||||||
case -20:
|
return NSLocalizedString("Unable to uninstall the app", comment: "")
|
||||||
return OperationError.noConnection
|
|
||||||
default:
|
case .CreateMisagent:
|
||||||
return OperationError.unknown
|
return self.createService(name: "misagent")
|
||||||
|
case .ProfileInstall:
|
||||||
|
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||||
|
case .ProfileRemove:
|
||||||
|
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func createService(name: String) -> String {
|
||||||
|
return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func getFromDevice(name: String) -> String {
|
||||||
|
return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func setArgument(name: String) -> String {
|
||||||
|
return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ private struct OTAUpdate
|
|||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
class PatchAppOperation: ResultOperation<Void>
|
final class PatchAppOperation: ResultOperation<Void>
|
||||||
{
|
{
|
||||||
let context: PatchAppContext
|
let context: PatchAppContext
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ extension PatchViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
class PatchViewController: UIViewController
|
final class PatchViewController: UIViewController
|
||||||
{
|
{
|
||||||
var patchApp: AnyApp?
|
var patchApp: AnyApp?
|
||||||
var installedApp: InstalledApp?
|
var installedApp: InstalledApp?
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import Roxas
|
|||||||
import minimuxer
|
import minimuxer
|
||||||
|
|
||||||
@objc(RefreshAppOperation)
|
@objc(RefreshAppOperation)
|
||||||
class RefreshAppOperation: ResultOperation<InstalledApp>
|
final class RefreshAppOperation: ResultOperation<InstalledApp>
|
||||||
{
|
{
|
||||||
let context: AppOperationContext
|
let context: AppOperationContext
|
||||||
|
|
||||||
@@ -49,15 +49,12 @@ class RefreshAppOperation: ResultOperation<InstalledApp>
|
|||||||
|
|
||||||
for p in profiles {
|
for p in profiles {
|
||||||
do {
|
do {
|
||||||
let x = try install_provisioning_profile(plist: p.value.data)
|
let bytes = p.value.data.toRustByteSlice()
|
||||||
if case .Bad(let code) = x {
|
try install_provisioning_profile(bytes.forRust())
|
||||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
|
||||||
}
|
|
||||||
} catch Uhoh.Bad(let code) {
|
|
||||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
|
||||||
} catch {
|
} catch {
|
||||||
self.finish(.failure(OperationError.unknown))
|
return self.finish(.failure(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
self.progress.completedUnitCount += 1
|
self.progress.completedUnitCount += 1
|
||||||
|
|
||||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
|
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import CoreData
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
|
|
||||||
class RefreshGroup: NSObject
|
final class RefreshGroup: NSObject
|
||||||
{
|
{
|
||||||
let context: AuthenticatedOperationContext
|
let context: AuthenticatedOperationContext
|
||||||
let progress = Progress.discreteProgress(totalUnitCount: 0)
|
let progress = Progress.discreteProgress(totalUnitCount: 0)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@objc(RemoveAppBackupOperation)
|
@objc(RemoveAppBackupOperation)
|
||||||
class RemoveAppBackupOperation: ResultOperation<Void>
|
final class RemoveAppBackupOperation: ResultOperation<Void>
|
||||||
{
|
{
|
||||||
let context: InstallAppOperationContext
|
let context: InstallAppOperationContext
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import AltStoreCore
|
|||||||
import minimuxer
|
import minimuxer
|
||||||
|
|
||||||
@objc(RemoveAppOperation)
|
@objc(RemoveAppOperation)
|
||||||
class RemoveAppOperation: ResultOperation<InstalledApp>
|
final class RemoveAppOperation: ResultOperation<InstalledApp>
|
||||||
{
|
{
|
||||||
let context: InstallAppOperationContext
|
let context: InstallAppOperationContext
|
||||||
|
|
||||||
@@ -39,15 +39,11 @@ class RemoveAppOperation: ResultOperation<InstalledApp>
|
|||||||
let resignedBundleIdentifier = installedApp.resignedBundleIdentifier
|
let resignedBundleIdentifier = installedApp.resignedBundleIdentifier
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let res = try remove_app(app_id: resignedBundleIdentifier)
|
try remove_app(resignedBundleIdentifier)
|
||||||
if case Uhoh.Bad(let code) = res {
|
|
||||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
|
||||||
}
|
|
||||||
} catch Uhoh.Bad(let code) {
|
|
||||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
|
||||||
} catch {
|
} catch {
|
||||||
self.finish(.failure(ALTServerError(.appDeletionFailed)))
|
return self.finish(.failure(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
self.progress.completedUnitCount += 1
|
self.progress.completedUnitCount += 1
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import AltStoreCore
|
|||||||
import AltSign
|
import AltSign
|
||||||
|
|
||||||
@objc(ResignAppOperation)
|
@objc(ResignAppOperation)
|
||||||
class ResignAppOperation: ResultOperation<ALTApplication>
|
final class ResignAppOperation: ResultOperation<ALTApplication>
|
||||||
{
|
{
|
||||||
let context: InstallAppOperationContext
|
let context: InstallAppOperationContext
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ class ResignAppOperation: ResultOperation<ALTApplication>
|
|||||||
{
|
{
|
||||||
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||||
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
|
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
|
||||||
|
print("Successfully resigned app to \(destinationURL.absoluteString)")
|
||||||
|
|
||||||
// Use appBundleURL since we need an app bundle, not .ipa.
|
// Use appBundleURL since we need an app bundle, not .ipa.
|
||||||
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||||
@@ -147,6 +148,14 @@ private extension ResignAppOperation
|
|||||||
infoDictionary[Bundle.Info.exportedUTIs] = exportedUTIs
|
infoDictionary[Bundle.Info.exportedUTIs] = exportedUTIs
|
||||||
|
|
||||||
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
|
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
|
||||||
|
|
||||||
|
// Remove _CodeSignature folder (if it exists) because it will be added when resigning and it may have files that aren't overwritten when resigning
|
||||||
|
// These files might be the cause of some ApplicationVerificationFailed errors
|
||||||
|
let codeSignaturePath = bundle.bundleURL.appendingPathComponent("_CodeSignature").absoluteString.replacingOccurrences(of: "file://", with: "")
|
||||||
|
if FileManager.default.fileExists(atPath: codeSignaturePath) {
|
||||||
|
try FileManager.default.removeItem(atPath: codeSignaturePath)
|
||||||
|
print("Removed _CodeSignature folder at \(codeSignaturePath)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.global().async {
|
DispatchQueue.global().async {
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import Foundation
|
|||||||
import Network
|
import Network
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
|
import minimuxer
|
||||||
|
|
||||||
@objc(SendAppOperation)
|
@objc(SendAppOperation)
|
||||||
class SendAppOperation: ResultOperation<()>
|
final class SendAppOperation: ResultOperation<()>
|
||||||
{
|
{
|
||||||
let context: InstallAppOperationContext
|
let context: InstallAppOperationContext
|
||||||
|
|
||||||
@@ -39,27 +40,23 @@ class SendAppOperation: ResultOperation<()>
|
|||||||
guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
|
|
||||||
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
|
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
|
||||||
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.url)
|
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL)
|
||||||
let fileURL = InstalledApp.refreshedIPAURL(for: app)
|
let fileURL = InstalledApp.refreshedIPAURL(for: app)
|
||||||
|
|
||||||
|
print("AFC App `fileURL`: \(fileURL.absoluteString)")
|
||||||
|
|
||||||
let ns_bundle = NSString(string: app.bundleIdentifier)
|
|
||||||
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
|
|
||||||
|
|
||||||
if let data = NSData(contentsOf: fileURL) {
|
if let data = NSData(contentsOf: fileURL) {
|
||||||
let pls = UnsafeMutablePointer<UInt8>.allocate(capacity: data.length)
|
do {
|
||||||
for (index, data) in data.enumerated() {
|
let bytes = Data(data).toRustByteSlice()
|
||||||
pls[index] = data
|
try yeet_app_afc(app.bundleIdentifier, bytes.forRust())
|
||||||
}
|
} catch {
|
||||||
let res = minimuxer_yeet_app_afc(ns_bundle_ptr, pls, UInt(data.length))
|
return self.finish(.failure(error))
|
||||||
if res == 0 {
|
|
||||||
self.progress.completedUnitCount += 1
|
|
||||||
self.finish(.success(()))
|
|
||||||
} else {
|
|
||||||
self.finish(.failure(minimuxer_to_operation(code: res)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.progress.completedUnitCount += 1
|
||||||
|
self.finish(.success(()))
|
||||||
} else {
|
} else {
|
||||||
|
print("IPA doesn't exist????")
|
||||||
self.finish(.failure(ALTServerError(.underlyingError)))
|
self.finish(.failure(ALTServerError(.underlyingError)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ extension UpdatePatronsOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpdatePatronsOperation: ResultOperation<Void>
|
final class UpdatePatronsOperation: ResultOperation<Void>
|
||||||
{
|
{
|
||||||
let context: NSManagedObjectContext
|
let context: NSManagedObjectContext
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ enum VerificationError: ALTLocalizedError
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc(VerifyAppOperation)
|
@objc(VerifyAppOperation)
|
||||||
class VerifyAppOperation: ResultOperation<Void>
|
final class VerifyAppOperation: ResultOperation<Void>
|
||||||
{
|
{
|
||||||
let context: AppOperationContext
|
let context: AppOperationContext
|
||||||
var verificationHandler: ((VerificationError) -> Bool)?
|
var verificationHandler: ((VerificationError) -> Bool)?
|
||||||
|
|||||||
23
AltStore/Protocols/Filterable.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// Filterable.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 01.12.22.
|
||||||
|
// Copyright © 2022 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol Filterable {
|
||||||
|
func matches(_ searchText: String) -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Collection where Element: Filterable {
|
||||||
|
func matches(_ searchText: String) -> Bool {
|
||||||
|
self.contains(where: { $0.matches(searchText) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func items(matching searchText: String) -> [Element] {
|
||||||
|
self.filter { $0.matches(searchText) }
|
||||||
|
}
|
||||||
|
}
|
||||||
22
AltStore/Protocols/NavigationTab.swift
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// NavigationTab.swift
|
||||||
|
// SideStoreUI
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 18.11.22.
|
||||||
|
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
protocol NavigationTab: RawRepresentable, Identifiable, CaseIterable, Hashable where RawValue == Int {
|
||||||
|
static var defaultTab: Self { get }
|
||||||
|
var displaySymbol: SFSymbol { get }
|
||||||
|
var displayName: String { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NavigationTab {
|
||||||
|
var id: Int {
|
||||||
|
self.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
11
AltStore/Protocols/ViewModel.swift
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//
|
||||||
|
// ViewModel.swift
|
||||||
|
// SideStoreUI
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 18.11.22.
|
||||||
|
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
protocol ViewModel: ObservableObject {}
|
||||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 846 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 912 B After Width: | Height: | Size: 997 B |